@webappwiz/cli 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jared Johnson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # @webappwiz/cli
2
+
3
+ Keeps a project in step with a webappwiz release.
4
+
5
+ ```bash
6
+ bunx @webappwiz/cli update # pin webappwiz deps, like bun update
7
+ bunx @webappwiz/cli skills ls # what there is, and what you have
8
+ bunx @webappwiz/cli skills add arbor # install an agent skill
9
+ bunx @webappwiz/cli skills update # refresh the ones already installed
10
+ bunx @webappwiz/cli rules ls # every rule there is
11
+ bunx @webappwiz/cli judge . # check a directory against them
12
+ bunx @webappwiz/cli signoff # does this change need a person?
13
+ ```
14
+
15
+ ## rules
16
+
17
+ Every rule webappwiz judges itself by is named in [`rules.ts`](./rules.ts), as
18
+ `JUDGE_RULES` and `SIGNOFF_RULES`, off the classes
19
+ [`@webappwiz/rules`](../rules/rules) ships. There is no config file and no
20
+ preset: a rule is in one of those lists or it does not exist.
21
+
22
+ ```
23
+ ID RULE SET LEVEL FILES
24
+ no-em-dashes No em dashes judge error **/*.ts
25
+ one-class-per-file One class per file judge error **/*.ts
26
+ visual-work-tested Visual work is tested signoff
27
+ ```
28
+
29
+ `rules show <id>` prints one in full: its glob, its level, and the document an
30
+ agent is handed verbatim.
31
+
32
+ The `SET` column is which of the two lists a rule is in. `judge` rules are what
33
+ `judge` checks files against, file by file. `signoff` rules have no glob and no
34
+ level because they are about a change rather than a file: `signoff` weighs them,
35
+ and what they answer is whether it needs a person rather than where the code is
36
+ wrong.
37
+
38
+ ## signoff
39
+
40
+ Weighs a change against the signoff rules and exits 1 with a reason when one of
41
+ them wants a person to look before it merges. One agent call over the whole
42
+ diff, since that is what these rules are about.
43
+
44
+ ```bash
45
+ bunx @webappwiz/cli signoff # everything since main
46
+ bunx @webappwiz/cli signoff --since HEAD~3 # measured against another ref
47
+ bunx @webappwiz/cli signoff --print # the rules, to apply yourself
48
+ ```
49
+
50
+ `--print` is the cheapest signoff there is: the agent about to merge reads the
51
+ rules and weighs its own change, spawning nothing. A project points its agent
52
+ instructions at that rather than at a list of rule ids, which goes stale the
53
+ next time a rule is added.
54
+
55
+ ## judge
56
+
57
+ Runs the rules over a directory, one agent call per set of rules sharing a set
58
+ of files.
59
+
60
+ ```bash
61
+ bunx @webappwiz/cli judge . --agent haiku
62
+ bunx @webappwiz/cli judge . --estimate # what would this read, and cost
63
+ bunx @webappwiz/cli judge . --print # print the prompts, spawn nothing
64
+ bunx @webappwiz/cli judge . --since main # only what changed
65
+ ```
66
+
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>`
72
+ comment above the line, or `rule-ignore-file <id>: <reason>` for the file.
73
+
74
+ ## update
75
+
76
+ Walks a directory for every `package.json` (workspaces, nested apps, anything)
77
+ and rewrites each webappwiz dependency to one version. They are released
78
+ together, so a project running two of them at different versions is running a
79
+ combination nobody tested.
80
+
81
+ The default version is this package's own, which is the point of `bunx`: the
82
+ release you invoke is the release you get. `--version` pins something else.
83
+ `workspace:` ranges are left alone; inside a monorepo they already track each
84
+ other.
85
+
86
+ ```bash
87
+ bunx @webappwiz/cli update ./apps --version 1.4.0
88
+ ```
89
+
90
+ ## skills
91
+
92
+ Puts the agent skills bundled with this package into `<dir>/.agents/skills/`.
93
+ Each skill's frontmatter carries the version it came from, so a stale copy is
94
+ visible rather than merely wrong.
95
+
96
+ ```bash
97
+ bunx @webappwiz/cli skills ls ./project
98
+ bunx @webappwiz/cli skills add arbor ./project
99
+ bunx @webappwiz/cli skills update ./project
100
+ ```
101
+
102
+ ```
103
+ SKILL SHIPS INSTALLED
104
+ arbor 1.4.0 1.3.0
105
+ other 1.4.0 -
106
+ ```
107
+
108
+ `add` installs one skill by name. `update` refreshes the ones a project already
109
+ has and never installs a new one: which skills a project uses is its own
110
+ business, and a skill nobody chose should not arrive by way of an update. `ls`
111
+ answers the one thing neither can: which version a project is actually holding.
112
+
113
+ Both replace what is there; local edits to a synced skill do not survive, and
114
+ are not meant to.
package/changed.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { type Ps } from "webappwiz/system";
2
+ /**
3
+ * The files git says are new or changed in `dir` since `ref`, named the way a
4
+ * rule's glob is: relative to `dir`, so a run over one package of a repo sees
5
+ * its own paths rather than the repo's.
6
+ *
7
+ * Deletions are left out. A violation quotes the offending line from disk, so a
8
+ * file that is gone has nothing to read and nothing to report.
9
+ */
10
+ export interface ChangedOptions {
11
+ /** What git is spawned through; the real process by default. */
12
+ ps?: Ps;
13
+ }
14
+ export declare function changed(dir: string, ref: string, opts?: ChangedOptions): Promise<Set<string>>;
15
+ /**
16
+ * The change itself, as a patch: everything in `dir` that differs from `ref`,
17
+ * committed or not, and the paths of the files git has never been told about,
18
+ * which no diff reaches.
19
+ *
20
+ * The new files are named rather than shown because a reader of this patch is
21
+ * an agent with the working directory in front of it, and it can open them.
22
+ */
23
+ export interface DiffOptions {
24
+ /** What git is spawned through; the real process by default. */
25
+ ps?: Ps;
26
+ }
27
+ export declare function diff(dir: string, ref: string, opts?: DiffOptions): Promise<{
28
+ patch: string;
29
+ added: string[];
30
+ }>;
package/cost.d.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { type Fs } from "webappwiz/system";
2
+ /** The agents an estimate can price, which is every one `--agent` accepts. */
3
+ export declare const priced: () => string[];
4
+ /**
5
+ * What those tokens cost to read on that agent, or undefined for an agent with
6
+ * no published price, which is any `--exec` command.
7
+ *
8
+ * A floor twice over: it counts input only, and only the input a plan can see.
9
+ */
10
+ export declare function floor(agent: string, tokens: number): number | undefined;
11
+ /**
12
+ * Dollars a single call costs beyond the files it was given, per agent.
13
+ *
14
+ * Everything the floor cannot see is charged per call, not per byte: the agent
15
+ * pays for its own system prompt every time it is spawned, and on this project
16
+ * that is most of a small call's bill. Measuring it per call is what lets a
17
+ * figure taken from a two-call run stand up to a fifteen-call one.
18
+ */
19
+ export type Overheads = Record<string, number>;
20
+ /**
21
+ * What past runs in `root` measured, empty until one has finished there. A
22
+ * missing or unreadable file is empty rather than an error: a calibration is a
23
+ * convenience, and losing it costs the caller a worse estimate, not a run.
24
+ */
25
+ export interface OverheadsOptions {
26
+ /** What the record is read through; the real filesystem by default. */
27
+ fs?: Fs;
28
+ }
29
+ export declare function overheads(dir: string, opts?: OverheadsOptions): Promise<Overheads>;
30
+ /**
31
+ * Records what a call on this agent costs over its floor, so the next
32
+ * `--estimate` has something better than one. Other agents are left alone,
33
+ * since each is a separate measurement.
34
+ */
35
+ export interface CalibrateOptions {
36
+ /** What the record is written through; the real filesystem by default. */
37
+ fs?: Fs;
38
+ }
39
+ export declare function calibrate(dir: string, agent: string, call: number, opts?: CalibrateOptions): Promise<void>;
40
+ /**
41
+ * What a plan costs on one agent: the price of the files it names, plus what
42
+ * every call charges on top. Undefined for an agent with no listed price, and
43
+ * the floor alone until a run has measured one.
44
+ */
45
+ export declare function predict(agent: string, tokens: number, calls: number, measured: Overheads): number | undefined;
@@ -0,0 +1,128 @@
1
+ import {
2
+ ask,
3
+ count,
4
+ diff,
5
+ divider,
6
+ mode,
7
+ overBudget,
8
+ tokens,
9
+ usd
10
+ } from "./index-jbrs6gqt.js";
11
+
12
+ // signoff.ts
13
+ import {
14
+ agentCommand,
15
+ Harness,
16
+ prompt as reviewPrompt
17
+ } from "@webappwiz/rules";
18
+ import { ConsoleLogger, color } from "webappwiz/log";
19
+ import { NodePs } from "webappwiz/system";
20
+ import { SystemClock } from "webappwiz/time";
21
+ class Signoff {
22
+ rules;
23
+ defaultAgent;
24
+ confirmer;
25
+ log;
26
+ ps;
27
+ clock;
28
+ constructor(rules, defaultAgent, opts = {}) {
29
+ this.rules = rules;
30
+ this.defaultAgent = defaultAgent;
31
+ this.confirmer = opts.confirmer ?? ask;
32
+ this.log = opts.log ?? new ConsoleLogger;
33
+ this.ps = opts.ps ?? new NodePs;
34
+ this.clock = opts.clock ?? new SystemClock;
35
+ }
36
+ async run(opts) {
37
+ if (mode(opts) === "print") {
38
+ this.print();
39
+ return;
40
+ }
41
+ const dir = opts.dir.replace(/\/+$/, "") || "/";
42
+ const { patch, added } = await diff(dir, opts.since, { ps: this.ps });
43
+ if (patch === "" && added.length === 0) {
44
+ this.log.info(`nothing has changed since ${opts.since}`);
45
+ return;
46
+ }
47
+ const agent = agentCommand(opts.exec === undefined ? { agent: opts.agent ?? this.defaultAgent } : opts);
48
+ 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
+ }
57
+ this.say(await this.judge(review, agent, dir));
58
+ }
59
+ async judge(review, agent, dir) {
60
+ const harness = new Harness({
61
+ log: this.log,
62
+ ps: this.ps,
63
+ clock: this.clock
64
+ });
65
+ harness.events.on("finished", ({ took, cost }) => {
66
+ const spent = cost === undefined ? "" : ` ${usd(cost)}`;
67
+ this.log.info(color.gray(`read in ${took.human()}${spent}`));
68
+ });
69
+ return await harness.run([review], agent, { cwd: dir });
70
+ }
71
+ say(findings) {
72
+ if (findings.length === 0) {
73
+ this.log.info(color.green("✓ nothing in this change needs a person"));
74
+ return;
75
+ }
76
+ for (const finding of findings) {
77
+ const where = finding.file === undefined ? "" : `${color.bold(finding.file + (finding.line === undefined ? "" : `:${finding.line}`))} `;
78
+ this.log.info(` ${where}${finding.message} ${color.gray(`(${finding.rule})`)}`);
79
+ }
80
+ throw new Error(`${count(findings.length, "reason")} to escalate rather than merge`);
81
+ }
82
+ review(patch, added, ref) {
83
+ const draft = {
84
+ rules: this.rules,
85
+ label: "signoff",
86
+ context: [
87
+ `The change, as \`git diff ${ref}\` prints it:`,
88
+ `\`\`\`diff
89
+ ${patch}
90
+ \`\`\``,
91
+ ...added.length === 0 ? [] : [
92
+ "These files are new and are in no diff yet. Read each one from " + "your working directory:",
93
+ added.map((file) => `- ${file}`).join(`
94
+ `)
95
+ ]
96
+ ].join(`
97
+
98
+ `),
99
+ instructions: WEIGHING
100
+ };
101
+ return { ...draft, bytes: Buffer.byteLength(reviewPrompt(draft)) };
102
+ }
103
+ print() {
104
+ if (this.rules.length === 0) {
105
+ this.log.info("no signoff rules");
106
+ return;
107
+ }
108
+ this.log.info("Weigh your change against each rule below. Anything that needs " + "review goes to a person instead of trunk.");
109
+ for (const rule of this.rules) {
110
+ this.log.info(`
111
+ ${divider(rule.id)}
112
+ `);
113
+ this.log.info(rule.document.trim());
114
+ }
115
+ this.log.info(`
116
+ ${divider()}`);
117
+ }
118
+ }
119
+ var WEIGHING = [
120
+ "These rules decide one thing: whether this change can merge on its own, " + "or needs a person to look at it first.",
121
+ "",
122
+ "Each rule's `Ships` section is what merges with nobody looking, and its " + "`Needs review` section is what does not. Report a violation only for " + "what the `Needs review` side covers, and only where the change " + "actually does it.",
123
+ "",
124
+ "These rules are about the change as a whole, so leave `file` and `line` " + "out unless one place in the change is the whole of the answer."
125
+ ].join(`
126
+ `);
127
+
128
+ export { Signoff };
@@ -0,0 +1,44 @@
1
+ // rules.ts
2
+ import { defineRules } from "@webappwiz/rules";
3
+ import {
4
+ ClassesOverFunctionExports,
5
+ CommentsSayWhyNotWhat,
6
+ DevServersFindAPort,
7
+ DocCommentsAddressUsers,
8
+ FakesOverMocks,
9
+ NamedOptionsLast,
10
+ NoEmDashes,
11
+ ObjectsOverCallbacks,
12
+ OneClassPerFile,
13
+ OneDirPerInterface,
14
+ ParametersDeclareFields,
15
+ ReactiveOverUseState,
16
+ ResourcesAreDisposable,
17
+ SimpleTestSetup,
18
+ TestsNotWeakened,
19
+ VisualWorkTested
20
+ } from "@webappwiz/rules/catalog";
21
+ var JUDGE_RULES = defineRules({
22
+ rules: [
23
+ new NoEmDashes,
24
+ new OneClassPerFile,
25
+ new ParametersDeclareFields,
26
+ new ClassesOverFunctionExports,
27
+ new ObjectsOverCallbacks,
28
+ new NamedOptionsLast,
29
+ new SimpleTestSetup,
30
+ new FakesOverMocks,
31
+ new CommentsSayWhyNotWhat,
32
+ new DocCommentsAddressUsers,
33
+ new OneDirPerInterface,
34
+ new DevServersFindAPort,
35
+ new ReactiveOverUseState,
36
+ new ResourcesAreDisposable
37
+ ]
38
+ });
39
+ var SIGNOFF_RULES = [
40
+ new TestsNotWeakened,
41
+ new VisualWorkTested
42
+ ];
43
+
44
+ export { JUDGE_RULES, SIGNOFF_RULES };
@@ -0,0 +1,229 @@
1
+ import {
2
+ table
3
+ } from "./index-jbrs6gqt.js";
4
+ // package.json
5
+ var version = "0.0.1";
6
+
7
+ // skills/add.ts
8
+ import { ConsoleLogger } from "webappwiz/log";
9
+ import { NodeFs } from "webappwiz/system";
10
+
11
+ // skills/skill.ts
12
+ import { dirname } from "node:path";
13
+
14
+ // templates/arbor.skill.md
15
+ var arbor_skill_default = `---
16
+ name: arbor
17
+ description: Use the @webappwiz/arbor CLI to land your work on trunk, or a base branch given as an argument, from an isolated git worktree without pull requests. Read this before making any code change in an arbor repository, since it decides where the work happens, and whenever you need to add, claim, merge, remove, list, show, locate, or escalate a task.
18
+ argument-hint: "[base-branch]"
19
+ version: 0.0.0
20
+ ---
21
+
22
+ # Using arbor
23
+
24
+ \`arbor\` runs many agents on one repo, each in its own git worktree, landing on
25
+ trunk without pull requests. Run it with \`bunx @webappwiz/arbor <command>\` (or
26
+ \`arbor\` if on PATH). \`arbor --help\` explains the commands; this file covers
27
+ only what the CLI cannot tell you.
28
+
29
+ **Rule:** never use raw git for state transitions arbor covers. Every landing
30
+ goes through \`arbor merge\`. The only exception is finishing an in-progress
31
+ rebase (\`git add\`, \`git rebase --continue\`), then merging again.
32
+
33
+ A failed command prints \`{reason}\` JSON on stdout and instructions on stderr:
34
+ do what stderr says. The one case to memorize is exit 4 \`lease_lost\`: stop,
35
+ do not retry, another agent owns the tree.
36
+
37
+ ## Before you start
38
+
39
+ Other agents may already be working. Before creating anything, list the files
40
+ you expect to touch, then \`arbor ls\`, and for each task in flight compare with
41
+ its changed files:
42
+ \`git -C "$(arbor path <task>)" diff --name-only main...task/<task>\`
43
+ (\`arbor show <task>\` for its plan; neither takes its lease).
44
+
45
+ If nothing overlaps, carry on. If something does, wait for it to land rather
46
+ than buying a rebase conflict: say one line about what you are waiting on,
47
+ then re-check periodically. A status is only true for the moment you read it,
48
+ so re-run \`arbor ls\` every time you are about to repeat one. Act on what it
49
+ becomes:
50
+
51
+ - gone from \`arbor ls\`: it landed. Redo the overlap check (trunk moved) and
52
+ carry on.
53
+ - \`escalated\`: your work is blocked on a person too. Tell the human what it
54
+ is blocked on and wait.
55
+ - \`orphaned\`, \`stray\`, \`unrecorded\` or \`unknown\` with nobody driving it: that
56
+ tree is broken. Say so and ask. (A tree mid-merge can read as \`orphaned\`
57
+ for a moment, so trust a broken status only if it survives a second look.)
58
+ - still \`working\` or \`merging\` after however long the human would tolerate
59
+ hearing nothing: offer the choice of waiting longer, working alongside, or
60
+ picking up something else, and say what you have not started.
61
+
62
+ A \`stale\` lease on a \`working\` task is normal (arbor only heartbeats while a
63
+ command runs): when waiting, watch a task's status, never its lease.
64
+
65
+ ## Workflow
66
+
67
+ 1. \`arbor add <task>\`, or \`arbor claim <task>\` to resume one. When this skill
68
+ is invoked with a branch argument (\`/arbor feature/auth\`), or the user
69
+ names the branch the work should land on, pass it as \`--base\` to every
70
+ task you create for that request. Otherwise omit \`--base\`; never guess a
71
+ base from the currently checked-out branch.
72
+ 2. Fill in the \`ARBOR.md\` stub \`add\` wrote at the worktree root (see below).
73
+ 3. Do the work; commit with git (arbor never commits for you).
74
+ 4. \`arbor merge\`. On failure, do what stderr says and merge again.
75
+
76
+ A successful merge deletes the worktree, and your working directory with it:
77
+ \`cd\` to the main tree (merge prints its path) before running anything else.
78
+ Then report it:
79
+
80
+ \`\`\`\`markdown
81
+ ### ✅ Merged \`<task>\` onto \`<base>\`
82
+
83
+ One sentence blending what the task set out to do with what actually changed.
84
+ \`\`\`\`
85
+
86
+ ✅ for a merge, ❌ for a task you \`arbor rm\`ed instead. Anything else worth
87
+ saying goes after this block, not instead of it.
88
+
89
+ ## ARBOR.md
90
+
91
+ Your session can die at any moment; \`ARBOR.md\` is what lets a stranger
92
+ \`arbor claim\` the task and continue. Fill in \`## Goal\` (one or two lines on
93
+ what done means) and list every step you can foresee under \`## Next\` as
94
+ \`- [ ]\` items, roughly one commit each. Move items to \`## Done\` as you finish
95
+ them: those checkboxes are the only progress the task reports. Decisions,
96
+ dead ends and how to verify go under \`## Notes\`. Update it as you go; a stale
97
+ plan is worse than none.
98
+
99
+ \`arbor show <task>\` prints the file and every way it departs from the
100
+ expected shape; run it on your own task after writing the file. \`add\` excludes
101
+ \`ARBOR.md\` from git for you: never commit it, and never mention it in a commit
102
+ message.
103
+
104
+ ## Committing
105
+
106
+ Plain, human-style commit messages with **no attribution**: no
107
+ \`Co-authored-by:\` trailers, no "Generated with", no agent or model names, no
108
+ \`--author\` overrides. Commit as often as it helps you; a task usually takes
109
+ fewer than 5 commits, and wanting many more means the task wants splitting,
110
+ not squashing.
111
+
112
+ ## Escalation
113
+
114
+ Merge only work you verified yourself. Escalate instead when verification
115
+ needs a person: external services, destructive migrations, anything tests
116
+ cannot confirm. And if the user asked to see the work before it lands,
117
+ escalate regardless.
118
+
119
+ 1. \`arbor escalate <reason>\`.
120
+ 2. Under \`## Blocked\` in \`ARBOR.md\`, state what needs verifying, ending in a
121
+ question a yes/no or a sentence can answer.
122
+ 3. Leave something the human can look at and print its **absolute path**
123
+ (start from \`arbor path <task>\`). For anything visual or UX, that means a
124
+ screenshot; if producing one is expensive or has side effects, ask before
125
+ starting and say what it will cost.
126
+
127
+ If you claim a tree whose \`## Blocked\` question is unanswered, do not resume
128
+ or merge: ask the user and wait for the answer.
129
+ `;
130
+
131
+ // skills/skill.ts
132
+ var bundled = { arbor: arbor_skill_default };
133
+ function versionOf(md) {
134
+ const frontmatter = md.match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
135
+ return frontmatter.match(/^version:\s*(.+)$/m)?.[1]?.trim() ?? null;
136
+ }
137
+ function available(skills) {
138
+ return Object.entries(skills).toSorted(([left], [right]) => left.localeCompare(right));
139
+ }
140
+ async function copy(name, doc, dir, opts) {
141
+ const target = `${dir}/.agents/skills/${name}/SKILL.md`;
142
+ await opts.fs.mkdir(dirname(target));
143
+ await opts.fs.write(target, doc);
144
+ opts.log.info(`wrote ${target}`);
145
+ }
146
+
147
+ // skills/add.ts
148
+ async function add(opts) {
149
+ const log = opts.log ?? new ConsoleLogger;
150
+ const fs = opts.fs ?? new NodeFs;
151
+ const skills = opts.skills ?? bundled;
152
+ const doc = skills[opts.skill];
153
+ if (doc === undefined) {
154
+ const have = available(skills).map(([name]) => name);
155
+ throw new Error(`no such skill: ${opts.skill} (have ${have.join(", ")})`);
156
+ }
157
+ await copy(opts.skill, doc, opts.dir, { log, fs });
158
+ }
159
+
160
+ // skills/ls.ts
161
+ import { ConsoleLogger as ConsoleLogger2, color } from "webappwiz/log";
162
+ import { NodeFs as NodeFs2 } from "webappwiz/system";
163
+ async function ls(opts) {
164
+ const log = opts.log ?? new ConsoleLogger2;
165
+ const fs = opts.fs ?? new NodeFs2;
166
+ const skills = opts.skills ?? bundled;
167
+ const rows = [["skill", "ships", "installed"].map(color.dim)];
168
+ let stale = 0;
169
+ for (const [name, doc] of available(skills)) {
170
+ const ships = versionOf(doc) ?? "?";
171
+ const installed = await fs.read(`${opts.dir}/.agents/skills/${name}/SKILL.md`).then(versionOf).catch(() => null);
172
+ if (installed !== null && installed !== ships) {
173
+ stale++;
174
+ }
175
+ rows.push([name, ships, installed ?? "-"]);
176
+ }
177
+ const lines = table(rows);
178
+ if (stale > 0) {
179
+ lines.push("", `${stale} out of date: run \`skills update\``);
180
+ }
181
+ log.info(lines.join(`
182
+ `));
183
+ }
184
+
185
+ // skills/update.ts
186
+ import { ConsoleLogger as ConsoleLogger3 } from "webappwiz/log";
187
+ import { NodeFs as NodeFs3 } from "webappwiz/system";
188
+ async function update(opts) {
189
+ const log = opts.log ?? new ConsoleLogger3;
190
+ const fs = opts.fs ?? new NodeFs3;
191
+ const installed = await fs.readdir(`${opts.dir}/.agents/skills`).catch(() => []);
192
+ const skills = opts.skills ?? bundled;
193
+ const ours = available(skills).filter(([name]) => installed.includes(name));
194
+ if (ours.length === 0) {
195
+ log.info(`no webappwiz skills in ${opts.dir}: add one with \`skills add\``);
196
+ return;
197
+ }
198
+ for (const [name, doc] of ours) {
199
+ await copy(name, doc, opts.dir, { log, fs });
200
+ }
201
+ }
202
+
203
+ // update.ts
204
+ import { basename } from "node:path";
205
+ import { ConsoleLogger as ConsoleLogger4 } from "webappwiz/log";
206
+ import { NodeFs as NodeFs4, walk } from "webappwiz/system";
207
+ var DEPENDENCY = /("(?:webappwiz|@webappwiz\/[^"]+)"\s*:\s*")(?!workspace:)[^"]*(")/g;
208
+ async function update2(opts) {
209
+ const log = opts.log ?? new ConsoleLogger4;
210
+ const fs = opts.fs ?? new NodeFs4;
211
+ let count = 0;
212
+ for await (const path of walk(opts.dir, { fs })) {
213
+ if (basename(path) !== "package.json") {
214
+ continue;
215
+ }
216
+ const before = await fs.read(path);
217
+ const after = before.replace(DEPENDENCY, `$1${opts.version}$2`);
218
+ if (after === before) {
219
+ continue;
220
+ }
221
+ await fs.write(path, after);
222
+ log.info(`updated ${path}`);
223
+ count++;
224
+ }
225
+ log.info(`${count} package.json pinned to ${opts.version}`);
226
+ await update({ dir: opts.dir, log, fs, skills: opts.skills });
227
+ }
228
+
229
+ export { version, add, ls, update, update2 as update1 };