@duyquangnvx/create-agent-cli 0.3.0 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duyquangnvx/create-agent-cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Creates a new Agent CLI repository wired to @duyquangnvx/agent-cli.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -19,7 +19,7 @@
19
19
  "access": "public"
20
20
  },
21
21
  "devDependencies": {
22
- "@duyquangnvx/agent-cli": "0.3.0",
22
+ "@duyquangnvx/agent-cli": "0.5.0",
23
23
  "tsdown": "0.23.0",
24
24
  "zod": "4.6.7"
25
25
  },
@@ -41,11 +41,13 @@ The line above should show 0.0.0, the version this skill describes. If it says {
41
41
  2. If it states a cost, ask the user before going on.
42
42
  3. Run it again without `--dry-run`, adding `--yes` once the user approved the cost.
43
43
 
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
+
44
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 hint gives the exact command.
45
47
 
46
48
  ## Recovering from errors
47
49
 
48
- Errors go to stderr and end with a `hint:` line naming the next step. Under `--json` they are `{ errors: [{ code, message, hint }] }`. Match the first row that fits:
50
+ Errors go to stderr and end with a `hint:` line naming the next step. Under `--json`, every stderr line is one JSON object: `{"progress":"..."}` while it runs, then `{ errors: [{ code, message, hint }] }` as the last line. Match the first row that fits:
49
51
 
50
52
  | Code or exit | What to do |
51
53
  |---|---|
@@ -1,4 +1,4 @@
1
- import { agentCli, cliError } from "@duyquangnvx/agent-cli";
1
+ import { agentCli, cliError, orStdin } from "@duyquangnvx/agent-cli";
2
2
  import * as z from "zod";
3
3
  import manifest from "../package.json" with { type: "json" };
4
4
  import { build } from "./build.js";
@@ -28,12 +28,16 @@ const greet = defineCommand({
28
28
  const card = defineCommand({
29
29
  name: "card",
30
30
  description: "Write a greeting card to <name>.txt",
31
+ help:
32
+ `A card in English is free; one in another language costs ${PRICE_PER_CARD} and needs --yes. ` +
33
+ "Give - as the message to read it from stdin: `{{binName}} card Ana - < note.txt`.",
31
34
  examples: [
32
35
  { command: "{{binName}} card Ana", note: "write Ana.txt with an English greeting" },
33
36
  {
34
37
  command: "{{binName}} card Ana --language es --dry-run",
35
38
  note: "show the file it would write and what it costs",
36
39
  },
40
+ { command: "{{binName}} card Ana --from Ben --from Cy", note: "sign the card from Ben and Cy" },
37
41
  ],
38
42
  effect: "write",
39
43
  args: z.object({
@@ -41,14 +45,24 @@ const card = defineCommand({
41
45
  .string()
42
46
  .regex(/^[\w-]+$/, "letters, digits, - and _ only")
43
47
  .describe("who the card is for; also the file's name"),
48
+ message: orStdin(z.string().min(1))
49
+ .optional()
50
+ .describe("a message to write under the greeting; - reads it from stdin"),
44
51
  }),
45
52
  options: z.object({
46
53
  language: z
47
54
  .string()
48
55
  .default(FREE_LANGUAGE)
49
56
  .describe("the greeting's language code, such as en, es or fr"),
57
+ from: z.array(z.string().min(1)).default([]).describe("who signs the card; repeat for each"),
58
+ }),
59
+ plan: ({ name, message, language, from }) => ({
60
+ name,
61
+ message,
62
+ language,
63
+ from,
64
+ file: `${name}.txt`,
50
65
  }),
51
- plan: ({ name, language }) => ({ name, language, file: `${name}.txt` }),
52
66
  outputs: (plan) => [plan.file],
53
67
  cost: (plan) =>
54
68
  plan.language === FREE_LANGUAGE
@@ -62,7 +76,12 @@ const card = defineCommand({
62
76
  hint: `run \`{{binName}} card ${plan.name} --language ${FREE_LANGUAGE}\` for an English card`,
63
77
  });
64
78
  }
65
- await ctx.files.write(plan.file, `${greeting}\n`);
79
+ const parts = [
80
+ greeting,
81
+ ...(plan.message === undefined ? [] : [plan.message.trimEnd()]),
82
+ ...(plan.from.length === 0 ? [] : [`From ${plan.from.join(", ")}`]),
83
+ ];
84
+ await ctx.files.write(plan.file, `${parts.join("\n\n")}\n`);
66
85
  return { file: plan.file, greeting };
67
86
  },
68
87
  render: ({ file }) => `Wrote ${file}`,
@@ -14,6 +14,22 @@ function fakeFiles(existing: readonly string[] = []) {
14
14
  write: async (path, data) => {
15
15
  written.set(path, String(data));
16
16
  },
17
+ read: async (path) => {
18
+ const text = written.get(path);
19
+ if (text === undefined) {
20
+ throw Object.assign(new Error(`ENOENT: no such file, open '${path}'`), { code: "ENOENT" });
21
+ }
22
+ return new TextEncoder().encode(text);
23
+ },
24
+ list: async () => [],
25
+ remove: async (path) => {
26
+ written.delete(path);
27
+ },
28
+ mkdir: async () => {},
29
+ stat: async (path) => {
30
+ const text = written.get(path);
31
+ return text === undefined ? undefined : { size: text.length, id: path };
32
+ },
17
33
  };
18
34
  return { files, written };
19
35
  }
@@ -30,8 +46,13 @@ function fakeGreetings() {
30
46
  return { greetings, calls };
31
47
  }
32
48
 
33
- function run(argv: string[], files = fakeFiles().files, greetings = fakeGreetings().greetings) {
34
- return runCli(cli, argv, { env: {}, cwd: CWD, files, ports: { greetings } });
49
+ function run(
50
+ argv: string[],
51
+ files = fakeFiles().files,
52
+ greetings = fakeGreetings().greetings,
53
+ stdin?: string,
54
+ ) {
55
+ return runCli(cli, argv, { env: {}, cwd: CWD, files, ports: { greetings }, stdin });
35
56
  }
36
57
 
37
58
  describe("greet", () => {
@@ -66,6 +87,35 @@ describe("card", () => {
66
87
  expect(written).toEqual(new Map([[resolve(CWD, "Ana.txt"), "Hi Ana\n"]]));
67
88
  });
68
89
 
90
+ test("- reads a message for under the greeting from stdin", async () => {
91
+ const { files, written } = fakeFiles();
92
+
93
+ const result = await run(
94
+ ["card", "Ana", "-"],
95
+ files,
96
+ undefined,
97
+ "Happy birthday!\nSee you soon.\n",
98
+ );
99
+
100
+ expect(result.exitCode).toBe(0);
101
+ expect(written.get(resolve(CWD, "Ana.txt"))).toBe("Hi Ana\n\nHappy birthday!\nSee you soon.\n");
102
+ });
103
+
104
+ test("--from, repeated, signs the card from each name", async () => {
105
+ const { files, written } = fakeFiles();
106
+
107
+ const result = await run(["card", "Ana", "--from", "Ben", "--from", "Cy"], files);
108
+
109
+ expect(result.exitCode).toBe(0);
110
+ expect(written.get(resolve(CWD, "Ana.txt"))).toBe("Hi Ana\n\nFrom Ben, Cy\n");
111
+ });
112
+
113
+ test("--help ends its options with a paragraph on cost and stdin", async () => {
114
+ const result = await run(["card", "--help"]);
115
+
116
+ expect(result.stdout).toMatch(/Options:[\s\S]*\n\nA card in English is free; /);
117
+ });
118
+
69
119
  test("refuses to overwrite an existing card without --force", async () => {
70
120
  const { files, written } = fakeFiles(["Ana.txt"]);
71
121