@wbern/claude-instructions 1.9.0 → 1.11.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 wbern
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 CHANGED
@@ -21,6 +21,10 @@ TDD workflow commands for Claude Code CLI.
21
21
 
22
22
  ```bash
23
23
  npx @wbern/claude-instructions
24
+
25
+ // or
26
+
27
+ pnpm dlx @wbern/claude-instructions
24
28
  ```
25
29
 
26
30
  The interactive installer lets you choose:
@@ -57,13 +61,14 @@ This ensures commands are regenerated whenever anyone runs `npm install`, `pnpm
57
61
 
58
62
  | Option | Description |
59
63
  |--------|-------------|
60
- | `--variant=with-beads` | Include Beads MCP integration |
61
- | `--variant=without-beads` | Standard commands only |
62
- | `--scope=project` | Install to `.claude/commands` in current directory |
63
- | `--scope=user` | Install to `~/.claude/commands` (global) |
64
- | `--prefix=my-` | Add prefix to command names (e.g., `my-commit.md`) |
65
- | `--skip-template-injection` | Don't inject CLAUDE.md template content |
64
+ | `--variant=with-beads` | Command variant (with-beads, without-beads) |
65
+ | `--scope=project` | Installation scope (project, user) |
66
+ | `--prefix=my-` | Add prefix to command names |
66
67
  | `--commands=commit,red,green` | Install only specific commands |
68
+ | `--skip-template-injection` | Skip injecting project CLAUDE.md customizations |
69
+ | `--update-existing` | Only update already-installed commands |
70
+ | `--overwrite` | Overwrite conflicting files without prompting |
71
+ | `--skip-on-conflict` | Skip conflicting files without prompting |
67
72
 
68
73
  ## Customizing Commands
69
74
 
@@ -190,14 +195,6 @@ flowchart TB
190
195
  - `/issue` - Analyze GitHub issue and create TDD implementation plan
191
196
  - `/plan` - Create implementation plan from feature/requirement with PRD-style discovery and TDD acceptance criteria
192
197
 
193
- ### Workflow
194
-
195
- - `/commit` - Create a git commit following project standards
196
- - `/busycommit` - Create multiple atomic git commits, one logical change at a time
197
- - `/pr` - Creates a pull request using GitHub MCP
198
- - `/summarize` - Summarize conversation progress and next steps
199
- - `/gap` - Analyze conversation context for unaddressed items and gaps
200
-
201
198
  ### Test-Driven Development
202
199
 
203
200
  - `/spike` - Execute TDD Spike Phase - exploratory coding to understand problem space before TDD
@@ -207,6 +204,15 @@ flowchart TB
207
204
  - `/refactor` - Execute TDD Refactor Phase - improve code structure while keeping tests green
208
205
  - `/cycle` - Execute complete TDD cycle - Red, Green, and Refactor phases in sequence
209
206
 
207
+ ### Workflow
208
+
209
+ - `/commit` - Create a git commit following project standards
210
+ - `/busycommit` - Create multiple atomic git commits, one logical change at a time
211
+ - `/pr` - Creates a pull request using GitHub MCP
212
+ - `/summarize` - Summarize conversation progress and next steps
213
+ - `/gap` - Analyze conversation context for unaddressed items and gaps
214
+ - `/code-review` - Code review using dynamic category detection and domain-specific analysis
215
+
210
216
  ### Ship / Show / Ask
211
217
 
212
218
  - `/ship` - Ship code directly to main - for small, obvious changes that don't need review
@@ -215,7 +221,7 @@ flowchart TB
215
221
 
216
222
  ### Worktree Management
217
223
 
218
- - `/worktree-add` - Add a new git worktree from branch name or GitHub issue URL, copy settings, install deps, and open in current IDE
224
+ - `/worktree-add` - Add a new git worktree from branch name or issue URL, copy settings, install deps, and open in current IDE
219
225
  - `/worktree-cleanup` - Clean up merged worktrees by verifying PR/issue status, consolidating settings, and removing stale worktrees
220
226
 
221
227
  ### Utilities
package/bin/cli.js CHANGED
@@ -119,6 +119,7 @@ init_esm_shims();
119
119
  import {
120
120
  select,
121
121
  text,
122
+ multiselect,
122
123
  groupMultiselect,
123
124
  isCancel,
124
125
  intro,
@@ -496,7 +497,7 @@ var CATEGORY_ORDER = [
496
497
  "Utilities",
497
498
  "Ship / Show / Ask"
498
499
  ];
499
- async function getCommandsGroupedByCategory(variant) {
500
+ async function loadCommandsMetadata(variant) {
500
501
  const sourcePath = path2.join(
501
502
  __dirname2,
502
503
  "..",
@@ -505,7 +506,10 @@ async function getCommandsGroupedByCategory(variant) {
505
506
  );
506
507
  const metadataPath = path2.join(sourcePath, "commands-metadata.json");
507
508
  const metadataContent = await fs.readFile(metadataPath, "utf-8");
508
- const metadata = JSON.parse(metadataContent);
509
+ return JSON.parse(metadataContent);
510
+ }
511
+ async function getCommandsGroupedByCategory(variant) {
512
+ const metadata = await loadCommandsMetadata(variant);
509
513
  const grouped = {};
510
514
  for (const [filename, data] of Object.entries(metadata)) {
511
515
  const category = data.category;
@@ -519,6 +523,11 @@ async function getCommandsGroupedByCategory(variant) {
519
523
  selectedByDefault: data.selectedByDefault !== false
520
524
  });
521
525
  }
526
+ for (const category of Object.keys(grouped)) {
527
+ if (!CATEGORY_ORDER.includes(category)) {
528
+ throw new Error(`Unknown category: ${category}`);
529
+ }
530
+ }
522
531
  for (const category of Object.keys(grouped)) {
523
532
  grouped[category].sort((a, b) => {
524
533
  const orderA = metadata[a.value].order;
@@ -527,12 +536,7 @@ async function getCommandsGroupedByCategory(variant) {
527
536
  });
528
537
  }
529
538
  const sortedCategories = Object.keys(grouped).sort((a, b) => {
530
- const indexA = CATEGORY_ORDER.indexOf(a);
531
- const indexB = CATEGORY_ORDER.indexOf(b);
532
- if (indexA !== -1 && indexB !== -1) return indexA - indexB;
533
- if (indexA !== -1) return -1;
534
- if (indexB !== -1) return 1;
535
- return a.localeCompare(b);
539
+ return CATEGORY_ORDER.indexOf(a) - CATEGORY_ORDER.indexOf(b);
536
540
  });
537
541
  const sortedGrouped = {};
538
542
  for (const category of sortedCategories) {
@@ -540,6 +544,37 @@ async function getCommandsGroupedByCategory(variant) {
540
544
  }
541
545
  return sortedGrouped;
542
546
  }
547
+ function extractLabelFromTool(tool) {
548
+ const match = tool.match(/^Bash\(([^:]+):/);
549
+ return match ? match[1] : tool;
550
+ }
551
+ function formatCommandsHint(commands) {
552
+ if (commands.length <= 2) {
553
+ return commands.map((c) => `/${c}`).join(", ");
554
+ }
555
+ const first = commands.slice(0, 2).map((c) => `/${c}`);
556
+ const remaining = commands.length - 2;
557
+ return `${first.join(", ")}, and ${remaining} ${remaining === 1 ? "other" : "others"}`;
558
+ }
559
+ async function getRequestedToolsOptions(variant) {
560
+ const metadata = await loadCommandsMetadata(variant);
561
+ const toolToCommands = /* @__PURE__ */ new Map();
562
+ for (const [filename, data] of Object.entries(metadata)) {
563
+ if (data["_requested-tools"]) {
564
+ const commandName = filename.replace(/\.md$/, "");
565
+ for (const tool of data["_requested-tools"]) {
566
+ const commands = toolToCommands.get(tool) || [];
567
+ commands.push(commandName);
568
+ toolToCommands.set(tool, commands);
569
+ }
570
+ }
571
+ }
572
+ return Array.from(toolToCommands.entries()).map(([tool, commands]) => ({
573
+ value: tool,
574
+ label: extractLabelFromTool(tool),
575
+ hint: formatCommandsHint(commands)
576
+ }));
577
+ }
543
578
  function getDestinationPath(outputPath, scope) {
544
579
  if (outputPath) {
545
580
  return outputPath;
@@ -596,6 +631,20 @@ async function generateToDirectory(outputPath, variant, scope, options) {
596
631
  } else {
597
632
  await fs.copy(sourcePath, destinationPath, {});
598
633
  }
634
+ if (options?.allowedTools && options.allowedTools.length > 0) {
635
+ for (const file of files) {
636
+ const filePath = path2.join(destinationPath, file);
637
+ const content = await fs.readFile(filePath, "utf-8");
638
+ const allowedToolsYaml = `allowed-tools: ${options.allowedTools.join(", ")}`;
639
+ const modifiedContent = content.replace(
640
+ /^---\n/,
641
+ `---
642
+ ${allowedToolsYaml}
643
+ `
644
+ );
645
+ await fs.writeFile(filePath, modifiedContent);
646
+ }
647
+ }
599
648
  if (options?.commandPrefix) {
600
649
  for (const file of files) {
601
650
  const oldPath = path2.join(destinationPath, file);
@@ -770,11 +819,26 @@ async function main(args) {
770
819
  let scope;
771
820
  let commandPrefix;
772
821
  let selectedCommands;
822
+ let selectedAllowedTools;
823
+ let cachedExistingFiles;
773
824
  if (args?.variant && args?.scope && args?.prefix !== void 0) {
774
825
  variant = args.variant;
775
826
  scope = args.scope;
776
827
  commandPrefix = args.prefix;
777
828
  selectedCommands = args.commands;
829
+ if (args.updateExisting) {
830
+ cachedExistingFiles = await checkExistingFiles(
831
+ void 0,
832
+ variant,
833
+ scope,
834
+ { commandPrefix: commandPrefix || "" }
835
+ );
836
+ selectedCommands = cachedExistingFiles.map((f) => f.filename);
837
+ if (selectedCommands.length === 0) {
838
+ log.warn("No existing commands found in target directory");
839
+ return;
840
+ }
841
+ }
778
842
  } else {
779
843
  variant = await select({
780
844
  message: "Select variant",
@@ -799,9 +863,34 @@ async function main(args) {
799
863
  if (isCancel(commandPrefix)) {
800
864
  return;
801
865
  }
802
- const groupedCommands = await getCommandsGroupedByCategory(
866
+ let groupedCommands = await getCommandsGroupedByCategory(
803
867
  variant
804
868
  );
869
+ if (args?.updateExisting) {
870
+ cachedExistingFiles = await checkExistingFiles(
871
+ void 0,
872
+ variant,
873
+ scope,
874
+ { commandPrefix: commandPrefix || "" }
875
+ );
876
+ const existingFilenames = new Set(
877
+ cachedExistingFiles.map((f) => f.filename)
878
+ );
879
+ const filteredGrouped = {};
880
+ for (const [category, commands] of Object.entries(groupedCommands)) {
881
+ const filtered = commands.filter(
882
+ (cmd) => existingFilenames.has(cmd.value)
883
+ );
884
+ if (filtered.length > 0) {
885
+ filteredGrouped[category] = filtered;
886
+ }
887
+ }
888
+ groupedCommands = filteredGrouped;
889
+ if (Object.keys(groupedCommands).length === 0) {
890
+ log.warn("No existing commands found in target directory");
891
+ return;
892
+ }
893
+ }
805
894
  const enabledCommandValues = Object.values(groupedCommands).flat().filter((cmd) => cmd.selectedByDefault).map((cmd) => cmd.value);
806
895
  selectedCommands = await groupMultiselect({
807
896
  message: "Select commands to install (Enter to accept all)",
@@ -811,32 +900,85 @@ async function main(args) {
811
900
  if (isCancel(selectedCommands)) {
812
901
  return;
813
902
  }
814
- }
815
- const existingFiles = await checkExistingFiles(
816
- void 0,
817
- variant,
818
- scope,
819
- {
820
- commandPrefix,
821
- commands: selectedCommands
903
+ const requestedToolsOptions = await getRequestedToolsOptions(
904
+ variant
905
+ );
906
+ if (requestedToolsOptions.length > 0) {
907
+ selectedAllowedTools = await multiselect({
908
+ message: "Select allowed tools for commands (optional)",
909
+ options: requestedToolsOptions,
910
+ required: false
911
+ });
912
+ if (isCancel(selectedAllowedTools)) {
913
+ return;
914
+ }
822
915
  }
823
- );
916
+ }
917
+ const existingFiles = cachedExistingFiles ?? await checkExistingFiles(void 0, variant, scope, {
918
+ commandPrefix,
919
+ commands: selectedCommands
920
+ });
824
921
  const skipFiles = [];
825
- for (const file of existingFiles) {
826
- if (file.isIdentical) {
827
- log.info(`${file.filename} is identical, skipping`);
828
- skipFiles.push(file.filename);
829
- continue;
922
+ if (!args?.overwrite && !args?.skipOnConflict) {
923
+ const conflictingFiles = existingFiles.filter((f) => !f.isIdentical);
924
+ const hasMultipleConflicts = conflictingFiles.length > 1;
925
+ let overwriteAllSelected = false;
926
+ let skipAllSelected = false;
927
+ for (const file of existingFiles) {
928
+ if (file.isIdentical) {
929
+ log.info(`${file.filename} is identical, skipping`);
930
+ skipFiles.push(file.filename);
931
+ continue;
932
+ }
933
+ if (overwriteAllSelected) {
934
+ continue;
935
+ }
936
+ if (skipAllSelected) {
937
+ skipFiles.push(file.filename);
938
+ continue;
939
+ }
940
+ const stats = getDiffStats(file.existingContent, file.newContent);
941
+ const diff = formatCompactDiff(file.existingContent, file.newContent);
942
+ note(diff, `Diff: ${file.filename}`);
943
+ log.info(`+${stats.added} -${stats.removed}`);
944
+ if (hasMultipleConflicts) {
945
+ const choice = await select({
946
+ message: `Overwrite ${file.filename}?`,
947
+ options: [
948
+ { value: "yes", label: "Yes" },
949
+ { value: "no", label: "No" },
950
+ { value: "overwrite_all", label: "Overwrite all" },
951
+ { value: "skip_all", label: "Skip all" }
952
+ ]
953
+ });
954
+ if (isCancel(choice)) {
955
+ return;
956
+ }
957
+ if (choice === "no") {
958
+ skipFiles.push(file.filename);
959
+ } else if (choice === "overwrite_all") {
960
+ overwriteAllSelected = true;
961
+ } else if (choice === "skip_all") {
962
+ skipAllSelected = true;
963
+ skipFiles.push(file.filename);
964
+ }
965
+ } else {
966
+ const shouldOverwrite = await confirm({
967
+ message: `Overwrite ${file.filename}?`
968
+ });
969
+ if (isCancel(shouldOverwrite)) {
970
+ return;
971
+ }
972
+ if (!shouldOverwrite) {
973
+ skipFiles.push(file.filename);
974
+ }
975
+ }
830
976
  }
831
- const stats = getDiffStats(file.existingContent, file.newContent);
832
- const diff = formatCompactDiff(file.existingContent, file.newContent);
833
- note(diff, `Diff: ${file.filename}`);
834
- log.info(`+${stats.added} -${stats.removed}`);
835
- const shouldOverwrite = await confirm({
836
- message: `Overwrite ${file.filename}?`
837
- });
838
- if (!shouldOverwrite) {
839
- skipFiles.push(file.filename);
977
+ } else if (args?.skipOnConflict) {
978
+ for (const file of existingFiles) {
979
+ if (!file.isIdentical) {
980
+ skipFiles.push(file.filename);
981
+ }
840
982
  }
841
983
  }
842
984
  const result = await generateToDirectory(
@@ -847,7 +989,8 @@ async function main(args) {
847
989
  commandPrefix,
848
990
  skipTemplateInjection: args?.skipTemplateInjection,
849
991
  commands: selectedCommands,
850
- skipFiles
992
+ skipFiles,
993
+ allowedTools: selectedAllowedTools
851
994
  }
852
995
  );
853
996
  const fullPath = scope === "project" ? `${process.cwd()}/.claude/commands` : `${os2.homedir()}/.claude/commands`;
@@ -860,36 +1003,111 @@ Happy TDD'ing!`
860
1003
  );
861
1004
  }
862
1005
 
863
- // scripts/bin.ts
864
- var STRING_ARGS = ["variant", "scope", "prefix"];
865
- var ARRAY_ARGS = ["commands"];
866
- var BOOLEAN_FLAGS = [
867
- { flag: "--skip-template-injection", key: "skipTemplateInjection" }
1006
+ // scripts/cli-options.ts
1007
+ init_esm_shims();
1008
+ var CLI_OPTIONS = [
1009
+ {
1010
+ flag: "--variant",
1011
+ key: "variant",
1012
+ type: "string",
1013
+ description: "Command variant (with-beads, without-beads)",
1014
+ example: "--variant=with-beads"
1015
+ },
1016
+ {
1017
+ flag: "--scope",
1018
+ key: "scope",
1019
+ type: "string",
1020
+ description: "Installation scope (project, user)",
1021
+ example: "--scope=project"
1022
+ },
1023
+ {
1024
+ flag: "--prefix",
1025
+ key: "prefix",
1026
+ type: "string",
1027
+ description: "Add prefix to command names",
1028
+ example: "--prefix=my-"
1029
+ },
1030
+ {
1031
+ flag: "--commands",
1032
+ key: "commands",
1033
+ type: "array",
1034
+ description: "Install only specific commands",
1035
+ example: "--commands=commit,red,green"
1036
+ },
1037
+ {
1038
+ flag: "--skip-template-injection",
1039
+ key: "skipTemplateInjection",
1040
+ type: "boolean",
1041
+ description: "Skip injecting project CLAUDE.md customizations"
1042
+ },
1043
+ {
1044
+ flag: "--update-existing",
1045
+ key: "updateExisting",
1046
+ type: "boolean",
1047
+ description: "Only update already-installed commands"
1048
+ },
1049
+ {
1050
+ flag: "--overwrite",
1051
+ key: "overwrite",
1052
+ type: "boolean",
1053
+ description: "Overwrite conflicting files without prompting"
1054
+ },
1055
+ {
1056
+ flag: "--skip-on-conflict",
1057
+ key: "skipOnConflict",
1058
+ type: "boolean",
1059
+ description: "Skip conflicting files without prompting"
1060
+ }
868
1061
  ];
1062
+ function generateHelpText() {
1063
+ const lines = [
1064
+ "Usage: npx @wbern/claude-instructions [options]",
1065
+ "",
1066
+ "Options:"
1067
+ ];
1068
+ for (const opt of CLI_OPTIONS) {
1069
+ const suffix = opt.type === "string" ? "=<value>" : opt.type === "array" ? "=<list>" : "";
1070
+ const padding = 28 - (opt.flag.length + suffix.length);
1071
+ lines.push(
1072
+ ` ${opt.flag}${suffix}${" ".repeat(Math.max(1, padding))}${opt.description}`
1073
+ );
1074
+ }
1075
+ lines.push(" --help, -h Show this help message");
1076
+ return lines.join("\n");
1077
+ }
1078
+
1079
+ // scripts/bin.ts
869
1080
  function parseArgs(argv) {
870
1081
  const args = {};
1082
+ const booleanOpts = CLI_OPTIONS.filter((o) => o.type === "boolean");
1083
+ const stringOpts = CLI_OPTIONS.filter((o) => o.type === "string");
1084
+ const arrayOpts = CLI_OPTIONS.filter((o) => o.type === "array");
871
1085
  for (const arg of argv) {
872
- for (const { flag, key } of BOOLEAN_FLAGS) {
873
- if (arg === flag) {
874
- args[key] = true;
1086
+ for (const opt of booleanOpts) {
1087
+ if (arg === opt.flag) {
1088
+ args[opt.key] = true;
875
1089
  }
876
1090
  }
877
- for (const key of STRING_ARGS) {
878
- const prefix = `--${key}=`;
1091
+ for (const opt of stringOpts) {
1092
+ const prefix = `${opt.flag}=`;
879
1093
  if (arg.startsWith(prefix)) {
880
- args[key] = arg.slice(prefix.length);
1094
+ args[opt.key] = arg.slice(prefix.length);
881
1095
  }
882
1096
  }
883
- for (const key of ARRAY_ARGS) {
884
- const prefix = `--${key}=`;
1097
+ for (const opt of arrayOpts) {
1098
+ const prefix = `${opt.flag}=`;
885
1099
  if (arg.startsWith(prefix)) {
886
- args[key] = arg.slice(prefix.length).split(",");
1100
+ args[opt.key] = arg.slice(prefix.length).split(",");
887
1101
  }
888
1102
  }
889
1103
  }
890
1104
  return args;
891
1105
  }
892
1106
  async function run(argv) {
1107
+ if (argv.includes("--help") || argv.includes("-h")) {
1108
+ console.log(generateHelpText());
1109
+ return;
1110
+ }
893
1111
  const args = parseArgs(argv);
894
1112
  await main(args);
895
1113
  }
@@ -17,6 +17,22 @@ Create multiple atomic git commits, committing the smallest possible logical uni
17
17
 
18
18
  Include any of the following info if specified: $ARGUMENTS
19
19
 
20
+ ## Commit Message Rules
21
+
22
+ Follows [Conventional Commits](https://www.conventionalcommits.org/) standard.
23
+
24
+ 1. **Format**: `type(#issue): description`
25
+ - Use `#123` for local repo issues
26
+ - Use `owner/repo#123` for cross-repo issues
27
+ - Common types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`
28
+
29
+ 2. **AI Credits**: **NEVER include AI credits in commit messages**
30
+ - No "Generated with Claude Code"
31
+ - No "Co-Authored-By: Claude" or "Co-Authored-By: Happy"
32
+ - Focus on the actual changes made, not conversation history
33
+
34
+ 3. **Content**: Write clear, concise commit messages describing what changed and why
35
+
20
36
  ## Process
21
37
 
22
38
  1. Run `git status` and `git diff` to review changes