@duyquangnvx/create-agent-cli 0.16.0 → 0.18.0
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/CHANGELOG.md +76 -0
- package/README.md +3 -3
- package/agents-section.md +5 -0
- package/dist/bin.js +2301 -1209
- package/package.json +4 -3
- package/partials/global/skillCheck.md +1 -1
- package/template/AGENTS.md +4 -7
- package/template/package.json +1 -1
- package/template/scripts/release.ts +1 -1
- package/template/skills/{{binName}}/SKILL.md +3 -3
- package/template/src/cli.ts +14 -26
- package/template/test/cli.test.ts +27 -15
- package/template/tsdown.config.ts +1 -1
- package/template/src/build.ts +0 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duyquangnvx/create-agent-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"dist",
|
|
14
14
|
"partials",
|
|
15
15
|
"template",
|
|
16
|
+
"agents-section.md",
|
|
16
17
|
"CHANGELOG.md"
|
|
17
18
|
],
|
|
18
19
|
"publishConfig": {
|
|
@@ -20,11 +21,11 @@
|
|
|
20
21
|
"access": "public"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
|
-
"@duyquangnvx/agent-cli": "0.
|
|
24
|
+
"@duyquangnvx/agent-cli": "0.18.0",
|
|
24
25
|
"tsdown": "0.23.0",
|
|
25
26
|
"zod": "4.6.7"
|
|
26
27
|
},
|
|
27
28
|
"scripts": {
|
|
28
|
-
"build": "tsdown"
|
|
29
|
+
"build": "node -e \"require('node:fs').copyFileSync('../agent-cli/agents-section.md', 'agents-section.md')\" && tsdown"
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
The `skill` row names every installed copy of this skill against the installed CLI. A missing copy, or one that states no version, is installed afresh with its `npx -y skills add` fix, since `npx skills update` skips a copy it did not install. After a fix that runs the `skills` installer, load this skill again.
|
|
1
|
+
The `skill` row names every installed copy of this skill against the installed CLI. A missing copy, or one that states no version, is installed afresh as the row says, with its `npx -y skills add` fix or from the page its message names, since `npx skills update` skips a copy it did not install. After a fix that runs the `skills` installer, load this skill again.
|
package/template/AGENTS.md
CHANGED
|
@@ -18,15 +18,12 @@ Greenfield with no deployments or users: change code directly. Breaking changes
|
|
|
18
18
|
|
|
19
19
|
## Coding standards
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- A Command that writes files names them in `outputs(plan)`, and the Runtime refuses to overwrite one without `--force`. A Command that costs money states it in `cost(plan)`, a sentence or `undefined` when cheap, and the Runtime refuses a costly run without `--yes`.
|
|
24
|
-
- A Command reaches a remote service through a port: an interface in `src/ports.ts`, built by the `ports` factory passed to `defineCli` and read as `ctx.ports`. It writes files through `ctx.files` and runs programs through `ctx.spawn`, so a test replaces each of them.
|
|
25
|
-
- Every error code is declared once in a `defineErrors` table passed to `defineCli({ errors })`, with the Exit class that must act on it and an optional `details` schema, and thrown through `cliError(errors, code, { message, hint })`, whose `hint` names the command to run next. An undeclared code, and anything else thrown, becomes an internal error (exit 70), whose stack trace `{{debugVariable}}=1` shows.
|
|
26
|
-
- A Command that lists many items declares its row schema as `rows` and returns `paginate(items, { limit, cursor })`, or `page(items, next)` with the source's own cursor when the source pages itself; the Runtime adds `--limit`, `--cursor` and `--fields`.
|
|
27
|
-
- Tests drive the CLI through `runCli`, replacing ports, files and the clock in its options, and assert stdout, stderr, the exit code and what the fakes recorded, as `test/cli.test.ts` does.
|
|
21
|
+
Follow the rules in `~/.claude/rules/harness/`. Biome enforces the mechanical ones.
|
|
22
|
+
|
|
28
23
|
- A change a user of `{{binName}}` would notice gets an entry under `## Unreleased` in `CHANGELOG.md`, in the format its opening paragraph states.
|
|
29
24
|
|
|
30
25
|
## Agent skills
|
|
31
26
|
|
|
32
27
|
`skills/{{binName}}/SKILL.md` is the agent skill: when to use `{{binName}}`, its workflows and how to recover from its errors. Update it with any change to those, and leave flag details to `--help`. Its `references/setup.md` installs, checks and updates `{{binName}}`; `test/cli.test.ts` fails when its install line, or the README's, differs from what `doctor` gives.
|
|
28
|
+
|
|
29
|
+
{{agentsSection}}
|
package/template/package.json
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@biomejs/biome": "2.5.15",
|
|
30
30
|
"@duyquangnvx/agent-cli": "{{runtimeVersion}}",
|
|
31
31
|
"@duyquangnvx/harness": "0.3.1",
|
|
32
|
-
"@duyquangnvx/release": "0.
|
|
32
|
+
"@duyquangnvx/release": "0.5.0",
|
|
33
33
|
"@types/node": "^22.19.0",
|
|
34
34
|
"tsdown": "0.23.0",
|
|
35
35
|
"typescript": "7.0.2",
|
|
@@ -43,17 +43,17 @@ The line above should show 0.0.0, the version this skill describes. If it says {
|
|
|
43
43
|
|
|
44
44
|
To put a message under the greeting, pass `-` after the name and pipe the message in with a heredoc, so its quotes and newlines reach the card intact.
|
|
45
45
|
|
|
46
|
-
Pass `--json` whenever you parse the output: stdout then holds only the data. A Command that changes something takes `--dry-run`, which prints its Plan, the files it writes and its cost, and changes nothing: run it first when unsure. A destructive Command also needs `--confirm=<target>`, and its error's
|
|
46
|
+
Pass `--json` whenever you parse the output: stdout then holds only the data. A Command that changes something takes `--dry-run`, which prints its Plan, the files it writes and its cost, and changes nothing: run it first when unsure. A destructive Command also needs `--confirm=<target>`, and its error's Next step gives the exact command.
|
|
47
47
|
|
|
48
48
|
## Recovering from errors
|
|
49
49
|
|
|
50
|
-
Errors go to stderr and end with a `hint:` line
|
|
50
|
+
Errors go to stderr and end with a `hint:` line, then a `run:` line for each command to run next, in order. Under `--json`, every stderr line is one JSON object: `{"progress":"..."}` or `{"notice":"..."}` while it runs, then `{ errors: [{ code, message, hint, next }] }` as the last line, `next` holding those commands when there are any. Match the first row that fits:
|
|
51
51
|
|
|
52
52
|
| Code or exit | What to do |
|
|
53
53
|
|---|---|
|
|
54
54
|
| `OUTPUT_EXISTS` | The run would overwrite a file. Ask the user before adding `--force`. |
|
|
55
55
|
| `NOT_CONFIRMED` | The run costs money. Tell the user the cost from the message; add `--yes` only once they approve it. |
|
|
56
|
-
| `LANGUAGE_UNSUPPORTED` | The greetings service lacks that language. Run the
|
|
56
|
+
| `LANGUAGE_UNSUPPORTED` | The greetings service lacks that language. Run the Next step's English card, or ask the user for another language. |
|
|
57
57
|
| exit 1 | You called it wrong. Fix what the message and hint point at, then run it again. |
|
|
58
58
|
| exit 2 | Something on this machine needs a person. Tell the user what the hint says and ask them to fix it. |
|
|
59
59
|
| exit 3 | The source it talks to is broken. Tell the user; running it again won't help. |
|
package/template/src/cli.ts
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
1
|
-
import { agentCli,
|
|
1
|
+
import { agentCli, orStdin } from "@duyquangnvx/agent-cli";
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import manifest from "../package.json" with { type: "json" };
|
|
4
|
-
import { build } from "./build.js";
|
|
5
4
|
import { errors } from "./errors.js";
|
|
6
|
-
import {
|
|
5
|
+
import { ports } from "./ports.js";
|
|
7
6
|
|
|
8
|
-
const { defineCli, defineCommand } = agentCli
|
|
7
|
+
const { defineCli, defineCommand } = agentCli({ errors, ports });
|
|
9
8
|
|
|
10
9
|
const FREE_LANGUAGE = "en";
|
|
11
10
|
const PRICE_PER_CARD = "$0.01";
|
|
12
11
|
|
|
13
12
|
const greet = defineCommand({
|
|
14
|
-
name: "greet",
|
|
15
13
|
description: "Greet someone by name",
|
|
16
14
|
examples: [
|
|
17
15
|
{
|
|
18
|
-
command: "{{
|
|
16
|
+
command: "{{binName}} greet Ana",
|
|
19
17
|
note: "say hello to Ana",
|
|
20
18
|
},
|
|
21
19
|
{
|
|
22
|
-
command: "{{
|
|
20
|
+
command: "{{binName}} greet Ana --json",
|
|
23
21
|
note: "get the greeting as JSON",
|
|
24
22
|
},
|
|
25
23
|
],
|
|
@@ -28,30 +26,29 @@ const greet = defineCommand({
|
|
|
28
26
|
name: z.string().min(1).describe("who to greet"),
|
|
29
27
|
}),
|
|
30
28
|
run: ({ name }) => ({ greeting: `Hello, ${name}!` }),
|
|
31
|
-
render: ({ greeting }) => greeting,
|
|
32
29
|
});
|
|
33
30
|
|
|
34
31
|
const card = defineCommand({
|
|
35
|
-
name: "card",
|
|
36
32
|
description: "Write a greeting card to <name>.txt",
|
|
37
33
|
help:
|
|
38
34
|
`A card in English is free; one in another language costs ${PRICE_PER_CARD} and needs --yes. ` +
|
|
39
35
|
"Give - as the message to read it from stdin: `{{runAs}} card Ana - < note.txt`.",
|
|
40
36
|
examples: [
|
|
41
37
|
{
|
|
42
|
-
command: "{{
|
|
38
|
+
command: "{{binName}} card Ana",
|
|
43
39
|
note: "write Ana.txt with an English greeting",
|
|
44
40
|
},
|
|
45
41
|
{
|
|
46
|
-
command: "{{
|
|
42
|
+
command: "{{binName}} card Ana --language es --dry-run",
|
|
47
43
|
note: "show the file it would write and what it costs",
|
|
48
44
|
},
|
|
49
45
|
{
|
|
50
|
-
command: "{{
|
|
46
|
+
command: "{{binName}} card Ana --from Ben --from Cy",
|
|
51
47
|
note: "sign the card from Ben and Cy",
|
|
52
48
|
},
|
|
53
49
|
],
|
|
54
50
|
effect: "write",
|
|
51
|
+
errors: ["LANGUAGE_UNSUPPORTED"],
|
|
55
52
|
args: z.object({
|
|
56
53
|
name: z
|
|
57
54
|
.string()
|
|
@@ -83,9 +80,10 @@ const card = defineCommand({
|
|
|
83
80
|
apply: async (plan, ctx) => {
|
|
84
81
|
const greeting = await ctx.ports.greetings.greet(plan.name, plan.language);
|
|
85
82
|
if (greeting === undefined) {
|
|
86
|
-
throw
|
|
83
|
+
throw ctx.fail("LANGUAGE_UNSUPPORTED", {
|
|
87
84
|
message: `the greetings service has no ${plan.language} greeting`,
|
|
88
|
-
hint:
|
|
85
|
+
hint: "run the Next step for an English card",
|
|
86
|
+
next: [["card", plan.name, "--language", FREE_LANGUAGE]],
|
|
89
87
|
});
|
|
90
88
|
}
|
|
91
89
|
const parts = [
|
|
@@ -101,17 +99,7 @@ const card = defineCommand({
|
|
|
101
99
|
});
|
|
102
100
|
|
|
103
101
|
export const cli = defineCli({
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
description: manifest.description,
|
|
107
|
-
build,
|
|
108
|
-
install: {
|
|
109
|
-
mode: "global",
|
|
110
|
-
package: manifest.name,
|
|
111
|
-
registry: manifest.publishConfig.registry,
|
|
112
|
-
},
|
|
113
|
-
engines: manifest.engines,
|
|
114
|
-
errors,
|
|
115
|
-
ports,
|
|
102
|
+
manifest,
|
|
103
|
+
install: "global",
|
|
116
104
|
commands: { greet, card },
|
|
117
105
|
});
|
|
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { type Files, runCli } from "@duyquangnvx/agent-cli";
|
|
6
|
+
import { checkExamples } from "@duyquangnvx/agent-cli/testing";
|
|
6
7
|
import { describe, expect, test } from "vitest";
|
|
7
8
|
import manifest from "../package.json" with { type: "json" };
|
|
8
9
|
import { cli } from "../src/cli.ts";
|
|
@@ -53,8 +54,11 @@ function fakeGreetings() {
|
|
|
53
54
|
return { greetings, calls };
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
function
|
|
57
|
-
return stderr
|
|
57
|
+
function nextOf(stderr: string): string[] {
|
|
58
|
+
return stderr
|
|
59
|
+
.split("\n")
|
|
60
|
+
.filter((line) => line.startsWith("run: "))
|
|
61
|
+
.map((line) => line.slice("run: ".length));
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
function run(
|
|
@@ -63,14 +67,18 @@ function run(
|
|
|
63
67
|
greetings = fakeGreetings().greetings,
|
|
64
68
|
stdin?: string,
|
|
65
69
|
) {
|
|
66
|
-
return runCli(cli, argv, {
|
|
70
|
+
return runCli(cli, argv, { cwd: CWD, files, ports: { greetings }, stdin });
|
|
67
71
|
}
|
|
68
72
|
|
|
73
|
+
test("every example parses, names a Command and passes its schemas", async () => {
|
|
74
|
+
await checkExamples(cli);
|
|
75
|
+
});
|
|
76
|
+
|
|
69
77
|
describe("greet", () => {
|
|
70
78
|
test("prints the greeting", async () => {
|
|
71
79
|
const result = await run(["greet", "Ana"]);
|
|
72
80
|
|
|
73
|
-
expect(result).toEqual({ stdout: "Hello, Ana
|
|
81
|
+
expect(result).toEqual({ stdout: 'greeting: "Hello, Ana!"\n', stderr: "", exitCode: 0 });
|
|
74
82
|
});
|
|
75
83
|
|
|
76
84
|
test("prints the data as JSON with --json", async () => {
|
|
@@ -133,7 +141,7 @@ describe("card", () => {
|
|
|
133
141
|
const refused = await run(["card", "Ana"], files);
|
|
134
142
|
|
|
135
143
|
expect(refused.exitCode).toBe(1);
|
|
136
|
-
expect(
|
|
144
|
+
expect(nextOf(refused.stderr)).toEqual([`${RUN_AS} card Ana --force`]);
|
|
137
145
|
expect(written.size).toBe(0);
|
|
138
146
|
|
|
139
147
|
const forced = await run(["card", "Ana", "--force"], files);
|
|
@@ -148,7 +156,7 @@ describe("card", () => {
|
|
|
148
156
|
const refused = await run(["card", "Ana", "--language", "es"], undefined, greetings);
|
|
149
157
|
|
|
150
158
|
expect(refused.exitCode).toBe(2);
|
|
151
|
-
expect(
|
|
159
|
+
expect(nextOf(refused.stderr)).toEqual([`${RUN_AS} card Ana --language es --yes`]);
|
|
152
160
|
expect(calls).toEqual([]);
|
|
153
161
|
|
|
154
162
|
const approved = await run(["card", "Ana", "--language", "es", "--yes"], undefined, greetings);
|
|
@@ -170,8 +178,8 @@ describe("card", () => {
|
|
|
170
178
|
describe("install lines", () => {
|
|
171
179
|
const root = fileURLToPath(new URL("..", import.meta.url));
|
|
172
180
|
|
|
173
|
-
/** Each doctor row
|
|
174
|
-
async function
|
|
181
|
+
/** Each doctor row, run from a pnpm project lacking the CLI, with no npm on PATH and no skill installed. */
|
|
182
|
+
async function doctorRows(): Promise<Record<string, { message: string; fix?: string }>> {
|
|
175
183
|
const home = await mkdtemp(join(tmpdir(), "{{binName}}-home-"));
|
|
176
184
|
const project = await mkdtemp(join(tmpdir(), "{{binName}}-project-"));
|
|
177
185
|
await writeFile(join(project, "package.json"), "{}\n");
|
|
@@ -181,8 +189,10 @@ describe("install lines", () => {
|
|
|
181
189
|
cwd: project,
|
|
182
190
|
bin: DOCTOR_BIN === "" ? undefined : join(root, DOCTOR_BIN),
|
|
183
191
|
});
|
|
184
|
-
const { rows } = JSON.parse(result.stdout) as {
|
|
185
|
-
|
|
192
|
+
const { rows } = JSON.parse(result.stdout) as {
|
|
193
|
+
rows: { check: string; message: string; fix?: string }[];
|
|
194
|
+
};
|
|
195
|
+
return Object.fromEntries(rows.map((row) => [row.check, row]));
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
async function text(path: string): Promise<string> {
|
|
@@ -190,20 +200,22 @@ describe("install lines", () => {
|
|
|
190
200
|
}
|
|
191
201
|
|
|
192
202
|
test("the setup reference and the README install the CLI with doctor's install line", async () => {
|
|
193
|
-
const
|
|
203
|
+
const install = (await doctorRows()).install?.fix;
|
|
194
204
|
|
|
195
205
|
expect(install).toBeDefined();
|
|
196
206
|
expect(await text("skills/{{binName}}/references/setup.md")).toContain(`\`! ${install}\``);
|
|
197
207
|
expect(await text("README.md")).toContain(`\`! ${install}\``);
|
|
198
208
|
});
|
|
199
209
|
|
|
210
|
+
// A hub that gives each user their own install line has no fix: the row's message names its page.
|
|
200
211
|
test.skipIf(cli.skill === undefined)(
|
|
201
|
-
"the README installs the skill
|
|
212
|
+
"the README installs the skill as doctor's skill row says",
|
|
202
213
|
async () => {
|
|
203
|
-
const {
|
|
214
|
+
const { fix, message } = (await doctorRows()).skill ?? { message: "" };
|
|
215
|
+
const line = fix === undefined ? message.split("; ").at(-1) : `\`! ${fix}\``;
|
|
204
216
|
|
|
205
|
-
expect(
|
|
206
|
-
expect(await text("README.md")).toContain(
|
|
217
|
+
expect(line).toMatch(/^`! |^copy your install line from /);
|
|
218
|
+
expect(await text("README.md")).toContain(line);
|
|
207
219
|
},
|
|
208
220
|
);
|
|
209
221
|
|
|
@@ -29,5 +29,5 @@ export default defineConfig({
|
|
|
29
29
|
fixedExtension: false,
|
|
30
30
|
// The bin bundles the Runtime and zod, so only this CLI's own release changes its behaviour.
|
|
31
31
|
deps: { onlyBundle: false },
|
|
32
|
-
define: {
|
|
32
|
+
define: { __AGENT_CLI_BUILD__: JSON.stringify(build) },
|
|
33
33
|
});
|
package/template/src/build.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { Cli } from "@duyquangnvx/agent-cli";
|
|
2
|
-
|
|
3
|
-
// tsdown.config.ts stamps this into the bundle; the source run unbundled, as in tests, has none.
|
|
4
|
-
declare const __BUILD__: Cli["build"];
|
|
5
|
-
|
|
6
|
-
export const build: Cli["build"] = typeof __BUILD__ === "undefined" ? undefined : __BUILD__;
|