Skip to content

Commit a44a5bc

Browse files
wbernclaude
andcommitted
feat: add --allowed-tools CLI flag and show automation command in outro
Add --allowed-tools CLI option to allow pre-approving tools for commands in non-interactive mode. Also display a helpful automation command in the outro after interactive mode completes, showing users how to reproduce their choices programmatically. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 83e6e1d commit a44a5bc

5 files changed

Lines changed: 157 additions & 1 deletion

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ This ensures commands are regenerated whenever anyone runs `npm install`, `pnpm
8282
| `--overwrite` | Overwrite conflicting files without prompting |
8383
| `--skip-on-conflict` | Skip conflicting files without prompting |
8484
| `--flags=beads,github` | Enable feature flags (beads, github, gitlab, etc.) |
85+
| `--allowed-tools=Bash(git diff:*),Bash(git status:*)` | Pre-approve tools for commands (non-interactive mode) |
8586
| `--help, -h` | Show help message |
8687
| `--version, -v` | Show version number |
8788

‎scripts/bin.test.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ describe("parseArgs", () => {
6464
flags: ["beads", "github"],
6565
});
6666
});
67+
68+
it("should parse --allowed-tools as comma-separated list", () => {
69+
const args = parseArgs([
70+
"--allowed-tools=Bash(git diff:*),Bash(git status:*)",
71+
]);
72+
73+
expect(args).toEqual({
74+
allowedTools: ["Bash(git diff:*)", "Bash(git status:*)"],
75+
});
76+
});
6777
});
6878

6979
describe("run", () => {
@@ -153,6 +163,7 @@ describe("CLI_OPTIONS consistency", () => {
153163
overwrite: undefined,
154164
skipOnConflict: undefined,
155165
flags: undefined,
166+
allowedTools: undefined,
156167
includeContribCommands: undefined,
157168
} satisfies Record<keyof Required<CliArgs>, undefined>;
158169

‎scripts/cli-options.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ export const CLI_OPTIONS: CliOption[] = [
6262
description: "Enable feature flags (beads, github, gitlab, etc.)",
6363
example: "--flags=beads,github",
6464
},
65+
{
66+
flag: "--allowed-tools",
67+
key: "allowedTools",
68+
type: "array",
69+
description: "Pre-approve tools for commands (non-interactive mode)",
70+
example: "--allowed-tools=Bash(git diff:*),Bash(git status:*)",
71+
},
6572
{
6673
flag: "--include-contrib-commands",
6774
key: "includeContribCommands",

‎scripts/cli.test.ts‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,96 @@ describe("CLI", () => {
254254
);
255255
});
256256

257+
it("should show automation command in outro for interactive mode", async () => {
258+
const { outro } = await import("@clack/prompts");
259+
const { main } = await import("./cli.js");
260+
261+
await setupInteractiveMocks({
262+
scope: "project",
263+
prefix: "my-",
264+
flags: ["beads"],
265+
commands: ["red.md", "green.md"],
266+
});
267+
268+
await main();
269+
270+
expect(outro).toHaveBeenCalledWith(
271+
expect.stringContaining("npx @wbern/claude-instructions"),
272+
);
273+
expect(outro).toHaveBeenCalledWith(
274+
expect.stringContaining("--scope=project"),
275+
);
276+
expect(outro).toHaveBeenCalledWith(expect.stringContaining("--prefix=my-"));
277+
expect(outro).toHaveBeenCalledWith(
278+
expect.stringContaining("--flags=beads"),
279+
);
280+
});
281+
282+
it("should include --commands in automation note when specific commands selected", async () => {
283+
const { outro } = await import("@clack/prompts");
284+
const { main } = await import("./cli.js");
285+
286+
await setupInteractiveMocks({
287+
scope: "project",
288+
commands: ["red.md", "green.md"],
289+
});
290+
291+
await main();
292+
293+
expect(outro).toHaveBeenCalledWith(
294+
expect.stringContaining("--commands=red.md,green.md"),
295+
);
296+
});
297+
298+
it("should NOT include --commands in automation note when no commands selected", async () => {
299+
const { outro } = await import("@clack/prompts");
300+
const { main } = await import("./cli.js");
301+
302+
await setupInteractiveMocks({
303+
scope: "project",
304+
commands: [],
305+
});
306+
307+
await main();
308+
309+
expect(outro).toHaveBeenCalledWith(
310+
expect.not.stringContaining("--commands="),
311+
);
312+
});
313+
314+
it("should include --allowed-tools in automation note when tools selected", async () => {
315+
const { outro } = await import("@clack/prompts");
316+
const { main } = await import("./cli.js");
317+
318+
await setupInteractiveMocks({
319+
scope: "project",
320+
allowedTools: ["Bash(git diff:*)", "Bash(git status:*)"],
321+
});
322+
323+
await main();
324+
325+
expect(outro).toHaveBeenCalledWith(
326+
expect.stringContaining(
327+
"--allowed-tools=Bash(git diff:*),Bash(git status:*)",
328+
),
329+
);
330+
});
331+
332+
it("should NOT show automation note in non-interactive mode", async () => {
333+
const { outro } = await import("@clack/prompts");
334+
const { main } = await import("./cli.js");
335+
336+
await main({
337+
scope: "project",
338+
prefix: "my-",
339+
flags: ["beads"],
340+
});
341+
342+
expect(outro).toHaveBeenCalledWith(
343+
expect.not.stringContaining("To automate this setup"),
344+
);
345+
});
346+
257347
it("should prompt for command prefix and pass it to generator", async () => {
258348
const { text } = await import("@clack/prompts");
259349
const { generateToDirectory } = await import("./cli-generator.js");
@@ -1784,4 +1874,23 @@ describe("non-TTY mode", () => {
17841874
}),
17851875
);
17861876
});
1877+
1878+
it("should pass allowedTools from CLI args to generator in non-interactive mode", async () => {
1879+
const { generateToDirectory } = await import("./cli-generator.js");
1880+
const { main } = await import("./cli.js");
1881+
1882+
await main({
1883+
scope: "project",
1884+
prefix: "",
1885+
allowedTools: ["Bash(git diff:*)", "Bash(git status:*)"],
1886+
});
1887+
1888+
expect(generateToDirectory).toHaveBeenCalledWith(
1889+
undefined,
1890+
"project",
1891+
expect.objectContaining({
1892+
allowedTools: ["Bash(git diff:*)", "Bash(git status:*)"],
1893+
}),
1894+
);
1895+
});
17871896
});

‎scripts/cli.ts‎

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ export interface CliArgs {
200200
overwrite?: boolean;
201201
skipOnConflict?: boolean;
202202
flags?: string[];
203+
allowedTools?: string[];
203204
includeContribCommands?: boolean;
204205
}
205206

@@ -219,6 +220,7 @@ export async function main(args?: CliArgs): Promise<void> {
219220
commandPrefix = args.prefix ?? "";
220221
selectedCommands = args.commands;
221222
selectedFlags = args.flags ? v.parse(FlagsSchema, args.flags) : undefined;
223+
selectedAllowedTools = args.allowedTools;
222224

223225
if (args.updateExisting) {
224226
cachedExistingFiles = await checkExistingFiles(undefined, scope, {
@@ -454,6 +456,32 @@ export async function main(args?: CliArgs): Promise<void> {
454456
? `${process.cwd()}/.claude/commands`
455457
: `${os.homedir()}/.claude/commands`;
456458

459+
// Build automation command for interactive mode
460+
const isInteractiveMode = !args?.scope;
461+
let automationNote = "";
462+
if (isInteractiveMode) {
463+
const parts = ["npx @wbern/claude-instructions"];
464+
parts.push(`--scope=${scope as string}`);
465+
if (commandPrefix) {
466+
parts.push(`--prefix=${commandPrefix as string}`);
467+
}
468+
if (selectedFlags && (selectedFlags as string[]).length > 0) {
469+
parts.push(`--flags=${(selectedFlags as string[]).join(",")}`);
470+
}
471+
if (selectedCommands && (selectedCommands as string[]).length > 0) {
472+
parts.push(`--commands=${(selectedCommands as string[]).join(",")}`);
473+
}
474+
if (selectedAllowedTools && (selectedAllowedTools as string[]).length > 0) {
475+
parts.push(
476+
`--allowed-tools=${(selectedAllowedTools as string[]).join(",")}`,
477+
);
478+
}
479+
automationNote = `
480+
481+
To automate this setup:
482+
${parts.join(" ")}`;
483+
}
484+
457485
outro(
458486
`Installed ${result.filesGenerated} commands to ${fullPath}
459487
@@ -465,7 +493,7 @@ Try it out:
465493
/red 1 returns "1" → Write first failing test for your kata
466494
/green → Make it pass
467495
468-
See a full example: https://github.com/wbern/claude-instructions#example-conversations
496+
See a full example: https://github.com/wbern/claude-instructions#example-conversations${automationNote}
469497
470498
Happy coding!`,
471499
);

0 commit comments

Comments
 (0)