@wbern/claude-instructions 1.12.0 → 1.14.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.
Files changed (2) hide show
  1. package/bin/cli.js +144 -97
  2. package/package.json +1 -1
package/bin/cli.js CHANGED
@@ -119,7 +119,6 @@ init_esm_shims();
119
119
  import {
120
120
  select,
121
121
  text,
122
- multiselect,
123
122
  groupMultiselect,
124
123
  isCancel,
125
124
  intro,
@@ -469,16 +468,39 @@ async function checkExistingFiles(outputPath, variant, scope, options) {
469
468
  if (!destinationPath) {
470
469
  return [];
471
470
  }
472
- const files = await fs.readdir(sourcePath);
471
+ const allFiles = await fs.readdir(sourcePath);
472
+ const files = options?.commands ? allFiles.filter((f) => options.commands.includes(f)) : allFiles;
473
473
  const existingFiles = [];
474
474
  const prefix = options?.commandPrefix || "";
475
+ let metadata = null;
476
+ let allowedToolsSet = null;
477
+ if (options?.allowedTools && options.allowedTools.length > 0) {
478
+ metadata = await loadCommandsMetadata(variant || VARIANTS.WITH_BEADS);
479
+ allowedToolsSet = new Set(options.allowedTools);
480
+ }
475
481
  for (const file of files) {
476
482
  const destFileName = prefix + file;
477
483
  const destFilePath = path2.join(destinationPath, destFileName);
478
484
  const sourceFilePath = path2.join(sourcePath, file);
479
485
  if (await fs.pathExists(destFilePath)) {
480
486
  const existingContent = await fs.readFile(destFilePath, "utf-8");
481
- const newContent = await fs.readFile(sourceFilePath, "utf-8");
487
+ let newContent = await fs.readFile(sourceFilePath, "utf-8");
488
+ if (metadata && allowedToolsSet) {
489
+ const commandMetadata = metadata[file];
490
+ const requestedTools = commandMetadata?.["_requested-tools"] || [];
491
+ const toolsForCommand = requestedTools.filter(
492
+ (tool) => allowedToolsSet.has(tool)
493
+ );
494
+ if (toolsForCommand.length > 0) {
495
+ const allowedToolsYaml = `allowed-tools: ${toolsForCommand.join(", ")}`;
496
+ newContent = newContent.replace(
497
+ /^---\n/,
498
+ `---
499
+ ${allowedToolsYaml}
500
+ `
501
+ );
502
+ }
503
+ }
482
504
  existingFiles.push({
483
505
  filename: destFileName,
484
506
  existingContent,
@@ -618,38 +640,42 @@ async function generateToDirectory(outputPath, variant, scope, options) {
618
640
  const allFiles = await fs.readdir(sourcePath);
619
641
  let files = options?.commands ? allFiles.filter((f) => options.commands.includes(f)) : allFiles;
620
642
  if (options?.skipFiles) {
621
- files = files.filter((f) => !options.skipFiles.includes(f));
643
+ const prefix2 = options?.commandPrefix || "";
644
+ files = files.filter((f) => !options.skipFiles.includes(prefix2 + f));
622
645
  }
623
- if (options?.commands || options?.skipFiles) {
646
+ const prefix = options?.commandPrefix || "";
647
+ if (options?.commands || options?.skipFiles || options?.commandPrefix) {
624
648
  await fs.ensureDir(destinationPath);
625
649
  for (const file of files) {
626
650
  await fs.copy(
627
651
  path2.join(sourcePath, file),
628
- path2.join(destinationPath, file)
652
+ path2.join(destinationPath, prefix + file)
629
653
  );
630
654
  }
631
655
  } else {
632
656
  await fs.copy(sourcePath, destinationPath, {});
633
657
  }
634
658
  if (options?.allowedTools && options.allowedTools.length > 0) {
659
+ const metadata = await loadCommandsMetadata(variant || VARIANTS.WITH_BEADS);
660
+ const allowedToolsSet = new Set(options.allowedTools);
635
661
  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
- `---
662
+ const commandMetadata = metadata[file];
663
+ const requestedTools = commandMetadata?.["_requested-tools"] || [];
664
+ const toolsForCommand = requestedTools.filter(
665
+ (tool) => allowedToolsSet.has(tool)
666
+ );
667
+ if (toolsForCommand.length > 0) {
668
+ const filePath = path2.join(destinationPath, prefix + file);
669
+ const content = await fs.readFile(filePath, "utf-8");
670
+ const allowedToolsYaml = `allowed-tools: ${toolsForCommand.join(", ")}`;
671
+ const modifiedContent = content.replace(
672
+ /^---\n/,
673
+ `---
642
674
  ${allowedToolsYaml}
643
675
  `
644
- );
645
- await fs.writeFile(filePath, modifiedContent);
646
- }
647
- }
648
- if (options?.commandPrefix) {
649
- for (const file of files) {
650
- const oldPath = path2.join(destinationPath, file);
651
- const newPath = path2.join(destinationPath, options.commandPrefix + file);
652
- await fs.rename(oldPath, newPath);
676
+ );
677
+ await fs.writeFile(filePath, modifiedContent);
678
+ }
653
679
  }
654
680
  }
655
681
  let templateInjected = false;
@@ -696,6 +722,88 @@ ${allowedToolsYaml}
696
722
  };
697
723
  }
698
724
 
725
+ // scripts/cli-options.ts
726
+ init_esm_shims();
727
+ var CLI_OPTIONS = [
728
+ {
729
+ flag: "--variant",
730
+ key: "variant",
731
+ type: "string",
732
+ description: "Command variant (with-beads, without-beads)",
733
+ example: "--variant=with-beads",
734
+ requiredForNonInteractive: true
735
+ },
736
+ {
737
+ flag: "--scope",
738
+ key: "scope",
739
+ type: "string",
740
+ description: "Installation scope (project, user)",
741
+ example: "--scope=project",
742
+ requiredForNonInteractive: true
743
+ },
744
+ {
745
+ flag: "--prefix",
746
+ key: "prefix",
747
+ type: "string",
748
+ description: "Add prefix to command names",
749
+ example: "--prefix=my-",
750
+ requiredForNonInteractive: true
751
+ },
752
+ {
753
+ flag: "--commands",
754
+ key: "commands",
755
+ type: "array",
756
+ description: "Install only specific commands",
757
+ example: "--commands=commit,red,green"
758
+ },
759
+ {
760
+ flag: "--skip-template-injection",
761
+ key: "skipTemplateInjection",
762
+ type: "boolean",
763
+ description: "Skip injecting project CLAUDE.md customizations"
764
+ },
765
+ {
766
+ flag: "--update-existing",
767
+ key: "updateExisting",
768
+ type: "boolean",
769
+ description: "Only update already-installed commands"
770
+ },
771
+ {
772
+ flag: "--overwrite",
773
+ key: "overwrite",
774
+ type: "boolean",
775
+ description: "Overwrite conflicting files without prompting"
776
+ },
777
+ {
778
+ flag: "--skip-on-conflict",
779
+ key: "skipOnConflict",
780
+ type: "boolean",
781
+ description: "Skip conflicting files without prompting"
782
+ }
783
+ ];
784
+ function generateHelpText() {
785
+ const lines = [
786
+ "Usage: npx @wbern/claude-instructions [options]",
787
+ "",
788
+ "Options:"
789
+ ];
790
+ for (const opt of CLI_OPTIONS) {
791
+ const suffix = opt.type === "string" ? "=<value>" : opt.type === "array" ? "=<list>" : "";
792
+ const padding = 28 - (opt.flag.length + suffix.length);
793
+ lines.push(
794
+ ` ${opt.flag}${suffix}${" ".repeat(Math.max(1, padding))}${opt.description}`
795
+ );
796
+ }
797
+ lines.push(" --help, -h Show this help message");
798
+ return lines.join("\n");
799
+ }
800
+
801
+ // scripts/tty.ts
802
+ init_esm_shims();
803
+ function isInteractiveTTY() {
804
+ return process.stdout.isTTY === true;
805
+ }
806
+
699
807
  // scripts/cli.ts
700
808
  var pc = process.env.FORCE_COLOR ? import_picocolors.default.createColors(true) : import_picocolors.default;
701
809
  function splitChangeIntoLines(value) {
@@ -840,6 +948,13 @@ async function main(args) {
840
948
  }
841
949
  }
842
950
  } else {
951
+ if (!isInteractiveTTY()) {
952
+ const requiredFlags = CLI_OPTIONS.filter(
953
+ (opt) => opt.requiredForNonInteractive
954
+ ).map((opt) => opt.flag).join(", ");
955
+ log.warn(`Non-interactive mode requires ${requiredFlags} arguments`);
956
+ return;
957
+ }
843
958
  variant = await select({
844
959
  message: "Select variant",
845
960
  options: [...VARIANT_OPTIONS]
@@ -904,9 +1019,11 @@ async function main(args) {
904
1019
  variant
905
1020
  );
906
1021
  if (requestedToolsOptions.length > 0) {
907
- selectedAllowedTools = await multiselect({
1022
+ selectedAllowedTools = await groupMultiselect({
908
1023
  message: "Select allowed tools for commands (optional)",
909
- options: requestedToolsOptions,
1024
+ options: {
1025
+ "All tools": requestedToolsOptions
1026
+ },
910
1027
  required: false
911
1028
  });
912
1029
  if (isCancel(selectedAllowedTools)) {
@@ -916,7 +1033,8 @@ async function main(args) {
916
1033
  }
917
1034
  const existingFiles = cachedExistingFiles ?? await checkExistingFiles(void 0, variant, scope, {
918
1035
  commandPrefix,
919
- commands: selectedCommands
1036
+ commands: selectedCommands,
1037
+ allowedTools: selectedAllowedTools
920
1038
  });
921
1039
  const skipFiles = [];
922
1040
  if (!args?.overwrite && !args?.skipOnConflict) {
@@ -999,83 +1117,12 @@ async function main(args) {
999
1117
 
1000
1118
  If Claude Code is already running, restart it to pick up the new commands.
1001
1119
 
1120
+ Try it out: /red clicking submit with empty email shows validation error
1121
+
1002
1122
  Happy TDD'ing!`
1003
1123
  );
1004
1124
  }
1005
1125
 
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
- }
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
1126
  // scripts/bin.ts
1080
1127
  function parseArgs(argv) {
1081
1128
  const args = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbern/claude-instructions",
3
- "version": "1.12.0",
3
+ "version": "1.14.0",
4
4
  "description": "TDD workflow commands for Claude Code CLI",
5
5
  "type": "module",
6
6
  "bin": "./bin/cli.js",