@webappwiz/cli 0.0.7 → 0.0.9

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-jbrs6gqt.js DELETED
@@ -1,418 +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 color3 } from "webappwiz/log";
9
- import {
10
- NodeFs as NodeFs2,
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 diff(dir, ref, opts = {}) {
35
- const ps = opts.ps ?? new NodePs;
36
- const patch = await git(ps, dir, ["diff", "--relative", ref]);
37
- const added = (await git(ps, dir, ["ls-files", "--others", "--exclude-standard"])).split(`
38
- `).filter((line) => line !== "");
39
- return { patch: patch.trim(), added };
40
- }
41
- async function git(ps, dir, argv) {
42
- const { exitCode, stdout, stderr } = await ps.spawnCapture([
43
- "git",
44
- "-C",
45
- dir,
46
- ...argv
47
- ]);
48
- if (exitCode !== 0) {
49
- throw new Error(`git ${argv[0]} failed in ${dir}: ${stderr.trim() || `exit ${exitCode}`}`);
50
- }
51
- return stdout;
52
- }
53
-
54
- // cost.ts
55
- import { join } from "node:path";
56
- import { NodeFs } from "webappwiz/system";
57
- var PRICES = { haiku: 1, sonnet: 3, opus: 5 };
58
- var priced = () => Object.keys(PRICES);
59
- function floor(agent, tokens) {
60
- const price = PRICES[agent];
61
- return price === undefined ? undefined : tokens / 1e6 * price;
62
- }
63
- var FILE = join(".wiz", "judge-cost.json");
64
- async function overheads(dir, opts = {}) {
65
- const fs = opts.fs ?? new NodeFs;
66
- try {
67
- const parsed = JSON.parse(await fs.read(join(dir, FILE)));
68
- if (typeof parsed !== "object" || parsed === null) {
69
- return {};
70
- }
71
- return Object.fromEntries(Object.entries(parsed).filter(([, call]) => typeof call === "number" && call > 0));
72
- } catch {
73
- return {};
74
- }
75
- }
76
- async function calibrate(dir, agent, call, opts = {}) {
77
- const fs = opts.fs ?? new NodeFs;
78
- const recorded = { ...await overheads(dir, { fs }), [agent]: call };
79
- await fs.mkdir(join(dir, ".wiz"));
80
- await fs.write(join(dir, FILE), `${JSON.stringify(recorded, null, "\t")}
81
- `);
82
- }
83
- function predict(agent, tokens, calls, measured) {
84
- const listed = floor(agent, tokens);
85
- const call = measured[agent];
86
- return listed === undefined || call === undefined ? listed : listed + call * calls;
87
- }
88
-
89
- // mode.ts
90
- function mode({ print, estimate, agent, exec }) {
91
- const named = [
92
- print === true ? "--print" : undefined,
93
- estimate === true ? "--estimate" : undefined,
94
- agent === undefined ? undefined : "--agent",
95
- exec === undefined ? undefined : "--exec"
96
- ].filter((flag) => flag !== undefined);
97
- if (named.length > 1) {
98
- throw new Error(`${named.join(" and ")} are different things to do with one run, so pass one`);
99
- }
100
- if (print === true) {
101
- return "print";
102
- }
103
- return estimate === true ? "estimate" : "run";
104
- }
105
-
106
- // report.ts
107
- import { color as color2 } from "webappwiz/log";
108
-
109
- // table.ts
110
- import { color } from "webappwiz/log";
111
- var table = (rows) => {
112
- const width = (cell) => color.strip(cell).length;
113
- const widths = rows[0]?.map((_, i) => Math.max(...rows.map((row) => width(row[i] ?? ""))));
114
- return rows.map((row) => row.map((cell, i) => cell.padEnd((widths?.[i] ?? 0) + cell.length - width(cell))).join(" ").trimEnd());
115
- };
116
-
117
- // report.ts
118
- var count = (total, word) => `${total} ${word}${total === 1 ? "" : "s"}`;
119
- var divider = (name) => {
120
- if (name === undefined) {
121
- return color2.dim("-".repeat(72));
122
- }
123
- const opening = `--- ${name} `;
124
- return color2.dim(opening.padEnd(Math.max(72, opening.length + 3), "-"));
125
- };
126
- var compact = new Intl.NumberFormat("en", { notation: "compact" });
127
- var money = new Intl.NumberFormat("en", {
128
- style: "currency",
129
- currency: "USD",
130
- minimumFractionDigits: 2,
131
- maximumFractionDigits: 4
132
- });
133
- var usd = (amount) => money.format(amount);
134
- var tokens = (bytes) => Math.ceil(bytes / 4);
135
- function overBudget(estimate, budget, cost) {
136
- const money2 = cost === undefined ? "" : ` That is ${color2.bold(usd(cost))} or more.`;
137
- return `${color2.yellow("!")} this run reads at least ` + `${color2.bold(compact.format(estimate))} tokens, over the ` + `${color2.bold(compact.format(budget))} budget, and the real cost will be higher.${money2} ` + `Raise it with ${color2.bold("--budget")}.`;
138
- }
139
- function estimate(files, rules, calls, tokens2, overheads2) {
140
- const agents = priced();
141
- const measured = agents.some((agent) => overheads2[agent] !== undefined);
142
- const rows = [
143
- ["agent", "floor", ...measured ? ["measured"] : []].map(color2.dim)
144
- ];
145
- for (const agent of agents) {
146
- const least = floor(agent, tokens2) ?? 0;
147
- const whole = overheads2[agent] === undefined ? undefined : predict(agent, tokens2, calls, overheads2);
148
- rows.push([
149
- agent,
150
- color2.green(`${usd(least)}+`),
151
- ...measured ? [whole === undefined ? "" : color2.green(usd(whole))] : []
152
- ]);
153
- }
154
- return [
155
- ...planned({ files, rules, calls, estimate: tokens2 }),
156
- ...table(rows).map((line) => ` ${line}`),
157
- "",
158
- color2.gray("floor: the listed input price for the files above, and nothing else. " + `Every call
159
- also pays for the agent's own system prompt and for whatever it re-reads.`),
160
- ...measured ? [
161
- color2.gray("measured: that floor plus what a call on the agent cost over it last time.")
162
- ] : [
163
- color2.gray("Run one of these and the estimate is measured against it next time.")
164
- ]
165
- ];
166
- }
167
- function planned({
168
- files,
169
- rules,
170
- calls,
171
- estimate: estimate2,
172
- concurrency,
173
- cost,
174
- agent
175
- }) {
176
- const rows = [
177
- [color2.dim("files"), String(files)],
178
- [color2.dim("rules"), String(rules)],
179
- [
180
- color2.dim("calls"),
181
- String(calls) + (concurrency === undefined ? "" : color2.dim(`, ${concurrency} at a time`))
182
- ],
183
- [color2.dim("reading"), `${compact.format(estimate2)}+ tokens`]
184
- ];
185
- if (cost !== undefined) {
186
- rows.push([color2.dim("cost"), color2.green(`${usd(cost)}+`)]);
187
- }
188
- if (agent !== undefined) {
189
- rows.push([color2.dim("agent"), agent]);
190
- }
191
- return ["", ...table(rows).map((line) => ` ${line}`), ""];
192
- }
193
- function finished({
194
- rules,
195
- files,
196
- violations,
197
- took,
198
- cost,
199
- done,
200
- total
201
- }) {
202
- const heading = `${color2.gray(`[${done}/${total}]`)} ` + color2.gray(`(${count(rules.length, "rule")}, ${count(files, "file")})`);
203
- const spent = cost === undefined ? "" : ` ${usd(cost)}`;
204
- const tail = `${color2.gray(`in ${took.human()}${spent}`)}`;
205
- if (violations.length === 0) {
206
- return [`${color2.green("✓")} ${heading}: clean ${tail}`];
207
- }
208
- return [
209
- `${color2.red("✗")} ${heading}: ${count(violations.length, "problem")} ${tail}`,
210
- ...violations.flatMap(finding)
211
- ];
212
- }
213
- function finding(violation) {
214
- const level = violation.level === "error" ? color2.red("error") : color2.yellow(violation.level);
215
- const lines = [
216
- ` ${color2.bold(`${violation.file}:${violation.line}`)} ${level} ${violation.message} ${color2.gray(`(${violation.id})`)}`
217
- ];
218
- if (violation.code !== "") {
219
- lines.push(color2.gray(` │ ${violation.code}`));
220
- }
221
- return lines;
222
- }
223
- function summary(violations, took, cost) {
224
- const spent = cost === undefined ? "" : ` ${usd(cost)} total`;
225
- const elapsed = color2.gray(`in ${took.human()}${spent}`);
226
- if (violations.length === 0) {
227
- return `${color2.green("✓ no violations")} ${elapsed}`;
228
- }
229
- const errors = violations.filter((violation) => violation.level === "error").length;
230
- const line = `✖ ${count(violations.length, "problem")} (${count(errors, "error")}, ${count(violations.length - errors, "warning")})`;
231
- return `${errors > 0 ? color2.red(line) : color2.yellow(line)} ${elapsed}`;
232
- }
233
-
234
- // judge.ts
235
- var estimated = (reviews) => tokens(reviews.reduce((bytes, review) => bytes + review.bytes, 0));
236
- var isFileRule = (rule) => ("files" in rule);
237
- var title = (rule) => /^#\s+(.+)$/m.exec(rule.document)?.[1]?.trim() ?? rule.id;
238
- var ask = {
239
- confirm: (question) => process.stdin.isTTY === true && /^y(es)?$/i.test((prompt(`${question} [y/N]`) ?? "").trim())
240
- };
241
-
242
- class JudgeCommands {
243
- rules;
244
- signoffRules;
245
- confirmer;
246
- log;
247
- fs;
248
- ps;
249
- clock;
250
- glob;
251
- constructor(rules, opts = {}) {
252
- this.rules = rules;
253
- this.signoffRules = opts.signoffRules ?? [];
254
- this.confirmer = opts.confirmer ?? ask;
255
- this.log = opts.log ?? new ConsoleLogger;
256
- this.fs = opts.fs ?? new NodeFs2;
257
- this.ps = opts.ps ?? new NodePs2;
258
- this.clock = opts.clock ?? new SystemClock;
259
- this.glob = opts.glob ?? new NodeGlob;
260
- }
261
- ls() {
262
- const rows = [["id", "rule", "set", "level", "files"].map(color3.dim)];
263
- for (const rule of this.rules.rules) {
264
- rows.push([rule.id, title(rule), "judge", rule.level, rule.files]);
265
- }
266
- for (const rule of this.signoffRules) {
267
- rows.push([rule.id, title(rule), "signoff", "", ""]);
268
- }
269
- this.log.info(table(rows).join(`
270
- `));
271
- }
272
- show(opts) {
273
- const all = [...this.rules.rules, ...this.signoffRules];
274
- const rule = all.find((candidate) => candidate.id === opts.id);
275
- if (!rule) {
276
- throw new Error(`no rule "${opts.id}". Known ids: ${all.map((candidate) => candidate.id).join(", ")}`);
277
- }
278
- const rows = [
279
- [color3.dim("id"), rule.id],
280
- [color3.dim("rule"), title(rule)]
281
- ];
282
- if (isFileRule(rule)) {
283
- rows.push([color3.dim("level"), rule.level], [color3.dim("files"), rule.files]);
284
- }
285
- this.log.info(table(rows).join(`
286
- `));
287
- this.log.info("");
288
- this.log.info(rule.document.trim());
289
- }
290
- async judge(opts) {
291
- const how = mode(opts);
292
- const config = this.rules;
293
- const rules = config.rules;
294
- const dir = opts.dir.replace(/\/+$/, "") || "/";
295
- const only = opts.since === undefined ? undefined : await changed(dir, opts.since, { ps: this.ps });
296
- if (only?.size === 0) {
297
- this.log.info(`nothing has changed since ${opts.since}`);
298
- return;
299
- }
300
- const files = new Files({ log: this.log, fs: this.fs, glob: this.glob });
301
- const reviews = await files.plan(rules, dir, {
302
- chunk: opts.chunk,
303
- only
304
- });
305
- if (how === "print") {
306
- for (const review of reviews) {
307
- this.log.info(`
308
- ${divider(`${review.label} (${count(review.files.length, "file")})`)}
309
- `);
310
- this.log.info(reviewPrompt(review));
311
- }
312
- this.log.info(`
313
- ${divider()}`);
314
- return;
315
- }
316
- const read = new Set(reviews.flatMap((review) => review.files)).size;
317
- const predicted = estimated(reviews);
318
- const calls = reviews.length;
319
- if (how === "estimate") {
320
- for (const line of estimate(read, rules.length, calls, predicted, await overheads(this.ps.cwd(), { fs: this.fs }))) {
321
- this.log.info(line);
322
- }
323
- return;
324
- }
325
- const agent = this.agent(config, opts);
326
- const model = this.model(config, opts);
327
- const root = this.ps.cwd();
328
- const measured = await overheads(root, { fs: this.fs });
329
- const started = this.clock.now();
330
- const cost = model === undefined ? undefined : predict(model, predicted, calls, measured);
331
- this.log.info(planned({
332
- files: read,
333
- rules: rules.length,
334
- calls,
335
- estimate: predicted,
336
- concurrency: config.concurrency,
337
- cost,
338
- agent: agent.label
339
- }).join(`
340
- `));
341
- if (predicted > opts.budget) {
342
- this.log.info(overBudget(predicted, opts.budget, cost));
343
- if (!await this.confirmer.confirm("Run anyway?")) {
344
- throw new Error("over budget");
345
- }
346
- }
347
- let spent = 0;
348
- let billed = false;
349
- const found = [];
350
- const harness = new Harness({
351
- log: this.log,
352
- ps: this.ps,
353
- clock: this.clock
354
- });
355
- harness.events.on("finished", (review) => {
356
- if (review.cost !== undefined) {
357
- spent += review.cost;
358
- billed = true;
359
- }
360
- const at = reviews[review.at];
361
- if (!at) {
362
- return;
363
- }
364
- const violations2 = files.violations(at, review.findings, dir);
365
- found[review.at] = violations2;
366
- for (const line of finished({
367
- rules: review.rules,
368
- files: at.files.length,
369
- violations: violations2,
370
- took: review.took,
371
- cost: review.cost,
372
- done: review.done,
373
- total: review.total
374
- })) {
375
- this.log.info(line);
376
- }
377
- });
378
- await harness.run(reviews, agent, {
379
- cwd: dir,
380
- concurrency: config.concurrency
381
- });
382
- const violations = found.flat();
383
- this.log.info("");
384
- this.log.info(summary(violations, this.clock.now().subtract(started), billed ? spent : undefined));
385
- await this.record(model, root, predicted, calls, billed ? spent : undefined);
386
- const errors = violations.filter((violation) => violation.level === "error").length;
387
- if (errors > 0) {
388
- throw new Error(count(errors, "error"));
389
- }
390
- }
391
- async record(agent, root, predicted, calls, spent) {
392
- if (agent === undefined || spent === undefined || calls <= 0) {
393
- return;
394
- }
395
- const listed = floor(agent, predicted);
396
- if (listed === undefined) {
397
- return;
398
- }
399
- const call = (spent - listed) / calls;
400
- if (call <= 0) {
401
- return;
402
- }
403
- try {
404
- await calibrate(root, agent, call, { fs: this.fs });
405
- } catch (error) {
406
- this.log.error(`could not record what this run cost: ${error}`);
407
- }
408
- }
409
- agent(config, opts) {
410
- const model = this.model(config, opts);
411
- return agentCommand(model === undefined ? opts : { agent: model });
412
- }
413
- model(config, opts) {
414
- return opts.exec === undefined ? opts.agent ?? config.agent : undefined;
415
- }
416
- }
417
-
418
- export { diff, mode, table, count, divider, usd, tokens, overBudget, ask, JudgeCommands };
package/signoff.d.ts DELETED
@@ -1,63 +0,0 @@
1
- import { type Rule } from "@webappwiz/rules";
2
- import { type Logger } from "webappwiz/log";
3
- import { type Ps } from "webappwiz/system";
4
- import { type Clock } from "webappwiz/time";
5
- import { type Confirm } from "./judge.js";
6
- export interface SignoffRunOptions {
7
- /** The directory whose change is being weighed. */
8
- dir: string;
9
- agent?: string;
10
- exec?: string;
11
- /** Print the rules to the logger and spawn nothing, for the reader who is
12
- * going to apply them itself. */
13
- print?: boolean;
14
- /** The ref the change is measured against: everything since it is the
15
- * change. */
16
- since: string;
17
- /** Tokens a run may read before it asks whether you meant it. */
18
- budget: number;
19
- }
20
- /**
21
- * Weighs a change against the rules that decide whether it can merge on its
22
- * own or needs a person to look at it first.
23
- *
24
- * One agent call over the diff, rather than one per file like `judge`: these
25
- * rules are about the change as a whole, and a run that saw one file at a time
26
- * could not answer them. Exits 1 with a reason when the change needs a person,
27
- * so it reads the same to a merge gate as to whoever ran it.
28
- */
29
- /** What a `Signoff` runs through. */
30
- export interface SignoffOptions {
31
- /** Who is asked before a run goes over budget; the terminal by default. */
32
- confirmer?: Confirm;
33
- log?: Logger;
34
- ps?: Ps;
35
- clock?: Clock;
36
- }
37
- export declare class Signoff {
38
- private rules;
39
- private defaultAgent;
40
- private confirmer;
41
- private log;
42
- private ps;
43
- private clock;
44
- constructor(rules: Rule[], defaultAgent: string, opts?: SignoffOptions);
45
- run(opts: SignoffRunOptions): Promise<void>;
46
- /** The one call, and what it says about the change. */
47
- private judge;
48
- /**
49
- * The verdict, and a reason for each rule that wants a person. Throws on any
50
- * of them: an agent that ran this before merging should escalate rather than
51
- * merge, and an exit code is what says so whoever is reading.
52
- */
53
- private say;
54
- /** The change as the agent reads it, priced by the prompt: what a new file
55
- * costs to open is the agent's business and unknowable from here. */
56
- private review;
57
- /**
58
- * The rules in full, for the reader who is going to apply them. Nothing has
59
- * to run these: an agent about to merge its own work can weigh it against
60
- * them itself, and that is the cheapest signoff there is.
61
- */
62
- private print;
63
- }
package/signoff.js DELETED
@@ -1,7 +0,0 @@
1
- import {
2
- Signoff
3
- } from "./index-3k3rtw49.js";
4
- import"./index-jbrs6gqt.js";
5
- export {
6
- Signoff
7
- };