@wbern/claude-instructions 2.0.0 → 2.2.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/README.md CHANGED
@@ -42,7 +42,7 @@ pnpm dlx @wbern/claude-instructions
42
42
 
43
43
  The interactive installer lets you choose:
44
44
 
45
- - **Variant**: With or without [Beads MCP](https://github.com/steveyegge/beads) integration
45
+ - **Feature flags**: Enable optional integrations like [Beads MCP](https://github.com/steveyegge/beads)
46
46
  - **Scope**: User-level (global) or project-level installation
47
47
 
48
48
  After installation, restart Claude Code if it's currently running.
@@ -60,7 +60,7 @@ Then add a postinstall script to your `package.json`:
60
60
  ```json
61
61
  {
62
62
  "scripts": {
63
- "postinstall": "npx @wbern/claude-instructions --variant=without-beads --scope=project --prefix="
63
+ "postinstall": "npx @wbern/claude-instructions --scope=project --overwrite"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@wbern/claude-instructions": "^1.0.0"
package/bin/cli.js CHANGED
@@ -47,17 +47,17 @@ var require_picocolors = __commonJS({
47
47
  var env = p.env || {};
48
48
  var isColorSupported = !(!!env.NO_COLOR || argv.includes("--no-color")) && (!!env.FORCE_COLOR || argv.includes("--color") || p.platform === "win32" || (p.stdout || {}).isTTY && env.TERM !== "dumb" || !!env.CI);
49
49
  var formatter = (open, close, replace = open) => (input) => {
50
- let string = "" + input, index = string.indexOf(close, open.length);
51
- return ~index ? open + replaceClose(string, close, replace, index) + close : open + string + close;
50
+ let string2 = "" + input, index = string2.indexOf(close, open.length);
51
+ return ~index ? open + replaceClose(string2, close, replace, index) + close : open + string2 + close;
52
52
  };
53
- var replaceClose = (string, close, replace, index) => {
53
+ var replaceClose = (string2, close, replace, index) => {
54
54
  let result = "", cursor = 0;
55
55
  do {
56
- result += string.substring(cursor, index) + replace;
56
+ result += string2.substring(cursor, index) + replace;
57
57
  cursor = index + close.length;
58
- index = string.indexOf(close, cursor);
58
+ index = string2.indexOf(close, cursor);
59
59
  } while (~index);
60
- return result + string.substring(cursor);
60
+ return result + string2.substring(cursor);
61
61
  };
62
62
  var createColors = (enabled = isColorSupported) => {
63
63
  let f = enabled ? formatter : () => String;
@@ -272,11 +272,11 @@ var Diff = class {
272
272
  return left === right || !!options.ignoreCase && left.toLowerCase() === right.toLowerCase();
273
273
  }
274
274
  }
275
- removeEmpty(array) {
275
+ removeEmpty(array2) {
276
276
  const ret = [];
277
- for (let i = 0; i < array.length; i++) {
278
- if (array[i]) {
279
- ret.push(array[i]);
277
+ for (let i = 0; i < array2.length; i++) {
278
+ if (array2[i]) {
279
+ ret.push(array2[i]);
280
280
  }
281
281
  }
282
282
  return ret;
@@ -387,6 +387,7 @@ function tokenize(value, options) {
387
387
 
388
388
  // scripts/cli.ts
389
389
  var import_picocolors = __toESM(require_picocolors(), 1);
390
+ import * as v2 from "valibot";
390
391
 
391
392
  // scripts/cli-generator.ts
392
393
  init_esm_shims();
@@ -397,8 +398,20 @@ import os from "os";
397
398
 
398
399
  // scripts/fragment-expander.ts
399
400
  init_esm_shims();
400
- import fs from "fs-extra";
401
+ import fs2 from "fs-extra";
401
402
  import path2 from "path";
403
+
404
+ // scripts/utils.ts
405
+ init_esm_shims();
406
+ import fs from "fs";
407
+ function getMarkdownFiles(dir) {
408
+ return fs.readdirSync(dir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
409
+ }
410
+ function getErrorMessage(err) {
411
+ return err instanceof Error ? err.message : String(err);
412
+ }
413
+
414
+ // scripts/fragment-expander.ts
402
415
  var TRANSFORM_BLOCK_REGEX = /<!--\s*docs\s+(\w+)([^>]*)-->([\s\S]*?)<!--\s*\/docs\s*-->/g;
403
416
  function parseOptions(attrString) {
404
417
  const options = {};
@@ -415,7 +428,7 @@ function expandContent(content, options) {
415
428
  TRANSFORM_BLOCK_REGEX,
416
429
  (_match, transformName, attrString) => {
417
430
  if (transformName !== "INCLUDE") {
418
- return "";
431
+ throw new Error(`Unknown transform type: ${transformName}`);
419
432
  }
420
433
  const attrs = parseOptions(attrString);
421
434
  const { path: includePath, featureFlag, elsePath } = attrs;
@@ -423,24 +436,24 @@ function expandContent(content, options) {
423
436
  if (elsePath) {
424
437
  const fullElsePath = path2.join(baseDir, elsePath);
425
438
  try {
426
- return fs.readFileSync(fullElsePath, "utf8");
439
+ return fs2.readFileSync(fullElsePath, "utf8");
427
440
  } catch (err) {
428
441
  throw new Error(
429
- `Failed to read elsePath '${elsePath}': ${err instanceof Error ? err.message : String(err)}`
442
+ `Failed to read elsePath '${elsePath}': ${getErrorMessage(err)}`
430
443
  );
431
444
  }
432
445
  }
433
446
  return "";
434
447
  }
435
448
  if (!includePath) {
436
- return "";
449
+ throw new Error("INCLUDE directive missing required 'path' attribute");
437
450
  }
438
451
  const fullPath = path2.join(baseDir, includePath);
439
452
  try {
440
- return fs.readFileSync(fullPath, "utf8");
453
+ return fs2.readFileSync(fullPath, "utf8");
441
454
  } catch (err) {
442
455
  throw new Error(
443
- `Failed to read '${includePath}': ${err instanceof Error ? err.message : String(err)}`
456
+ `Failed to read '${includePath}': ${getErrorMessage(err)}`
444
457
  );
445
458
  }
446
459
  }
@@ -452,6 +465,7 @@ init_esm_shims();
452
465
  import fs3 from "fs";
453
466
  import path3 from "path";
454
467
  import { fileURLToPath as fileURLToPath2 } from "url";
468
+ import * as v from "valibot";
455
469
 
456
470
  // scripts/cli-options.ts
457
471
  init_esm_shims();
@@ -508,6 +522,13 @@ var CLI_OPTIONS = [
508
522
  type: "array",
509
523
  description: "Enable feature flags (beads, github, gitlab, etc.)",
510
524
  example: "--flags=beads,github"
525
+ },
526
+ {
527
+ flag: "--include-contrib-commands",
528
+ key: "includeContribCommands",
529
+ type: "boolean",
530
+ description: "Include underscore-prefixed contributor commands",
531
+ internal: true
511
532
  }
512
533
  ];
513
534
  function generateHelpText() {
@@ -528,13 +549,6 @@ function generateHelpText() {
528
549
  return lines.join("\n");
529
550
  }
530
551
 
531
- // scripts/utils.ts
532
- init_esm_shims();
533
- import fs2 from "fs";
534
- function getMarkdownFiles(dir) {
535
- return fs2.readdirSync(dir).filter((f) => f.endsWith(".md"));
536
- }
537
-
538
552
  // scripts/generate-readme.ts
539
553
  var __filename2 = fileURLToPath2(import.meta.url);
540
554
  var __dirname2 = path3.dirname(__filename2);
@@ -549,6 +563,15 @@ var CATEGORIES = {
549
563
  WORKTREE: "Worktree Management",
550
564
  UTILITIES: "Utilities"
551
565
  };
566
+ var CategoryValues = Object.values(CATEGORIES);
567
+ var CategorySchema = v.picklist(CategoryValues);
568
+ var RequiredFrontmatterSchema = v.object({
569
+ description: v.pipe(v.string(), v.minLength(1)),
570
+ _order: v.number()
571
+ });
572
+ var IncludeOptionsSchema = v.object({
573
+ path: v.string()
574
+ });
552
575
  function parseFrontmatter(content) {
553
576
  const match = content.match(FRONTMATTER_REGEX);
554
577
  if (!match) return {};
@@ -591,7 +614,8 @@ function parseFrontmatter(content) {
591
614
  return frontmatter;
592
615
  }
593
616
  function getCategory(frontmatter) {
594
- return frontmatter._category || CATEGORIES.UTILITIES;
617
+ const category = frontmatter._category || CATEGORIES.UTILITIES;
618
+ return v.parse(CategorySchema, category);
595
619
  }
596
620
  function generateCommandsMetadata() {
597
621
  const sourcesDir = path3.join(PROJECT_ROOT, SOURCES_DIR);
@@ -600,12 +624,13 @@ function generateCommandsMetadata() {
600
624
  for (const file of files) {
601
625
  const content = fs3.readFileSync(path3.join(sourcesDir, file), "utf8");
602
626
  const frontmatter = parseFrontmatter(content);
627
+ const validated = v.parse(RequiredFrontmatterSchema, frontmatter);
603
628
  const requestedTools = frontmatter[REQUESTED_TOOLS_KEY];
604
629
  metadata[file] = {
605
- description: frontmatter.description || "No description",
630
+ description: validated.description,
606
631
  hint: frontmatter._hint,
607
632
  category: getCategory(frontmatter),
608
- order: typeof frontmatter._order === "number" ? frontmatter._order : 999,
633
+ order: validated._order,
609
634
  ...frontmatter._selectedByDefault === false ? { selectedByDefault: false } : {},
610
635
  ...requestedTools ? { [REQUESTED_TOOLS_KEY]: requestedTools } : {}
611
636
  };
@@ -614,6 +639,8 @@ function generateCommandsMetadata() {
614
639
  }
615
640
 
616
641
  // scripts/cli-generator.ts
642
+ import { lint } from "markdownlint/sync";
643
+ import { applyFixes } from "markdownlint";
617
644
  var __filename3 = fileURLToPath3(import.meta.url);
618
645
  var __dirname3 = path4.dirname(__filename3);
619
646
  var SCOPES = {
@@ -628,6 +655,18 @@ var DIRECTORIES = {
628
655
  var TEMPLATE_SOURCE_FILES = ["CLAUDE.md", "AGENTS.md"];
629
656
  var REQUESTED_TOOLS_KEY = "_requested-tools";
630
657
  var ELLIPSIS = "...";
658
+ function isSourceFile(filename, includeContribCommands) {
659
+ return filename.endsWith(".md") && (includeContribCommands || !filename.startsWith("_"));
660
+ }
661
+ function stripContribPrefix(filename) {
662
+ return filename.startsWith("_") ? filename.slice(1) : filename;
663
+ }
664
+ async function getSourceFiles(includeContribCommands) {
665
+ const sourcePath = path4.join(__dirname3, "..", DIRECTORIES.SOURCES);
666
+ return (await fs4.readdir(sourcePath)).filter(
667
+ (f) => isSourceFile(f, includeContribCommands)
668
+ );
669
+ }
631
670
  function truncatePathFromLeft(pathStr, maxLength) {
632
671
  if (pathStr.length <= maxLength) {
633
672
  return pathStr;
@@ -645,19 +684,14 @@ var FLAG_OPTIONS = [
645
684
  label: "Beads MCP",
646
685
  hint: "Local issue tracking",
647
686
  category: "Feature Flags"
687
+ },
688
+ {
689
+ value: "no-plan-files",
690
+ label: "No Plan Files",
691
+ hint: "Forbid Claude Code's internal plan.md",
692
+ category: "Feature Flags"
648
693
  }
649
694
  ];
650
- function getFlagsGroupedByCategory() {
651
- const grouped = {};
652
- for (const flag of FLAG_OPTIONS) {
653
- const { category, ...option } = flag;
654
- if (!grouped[category]) {
655
- grouped[category] = [];
656
- }
657
- grouped[category].push(option);
658
- }
659
- return grouped;
660
- }
661
695
  function getScopeOptions(terminalWidth = 80) {
662
696
  const projectPath = path4.join(
663
697
  process.cwd(),
@@ -684,9 +718,9 @@ function getScopeOptions(terminalWidth = 80) {
684
718
  }
685
719
  async function checkExistingFiles(outputPath, scope, options) {
686
720
  const sourcePath = path4.join(__dirname3, "..", DIRECTORIES.SOURCES);
687
- const destinationPath = outputPath || getDestinationPath(outputPath, scope);
721
+ const destinationPath = getDestinationPath(outputPath, scope);
688
722
  const flags = options?.flags ?? [];
689
- const allFiles = await fs4.readdir(sourcePath);
723
+ const allFiles = await getSourceFiles(options?.includeContribCommands);
690
724
  const files = options?.commands ? allFiles.filter((f) => options.commands.includes(f)) : allFiles;
691
725
  const existingFiles = [];
692
726
  const prefix = options?.commandPrefix || "";
@@ -698,16 +732,21 @@ async function checkExistingFiles(outputPath, scope, options) {
698
732
  }
699
733
  const baseDir = path4.join(__dirname3, "..");
700
734
  for (const file of files) {
701
- const destFileName = prefix + file;
735
+ const outputFileName = stripContribPrefix(file);
736
+ const destFileName = prefix + outputFileName;
702
737
  const destFilePath = path4.join(destinationPath, destFileName);
703
738
  const sourceFilePath = path4.join(sourcePath, file);
704
739
  if (await fs4.pathExists(destFilePath)) {
705
740
  const existingContent = await fs4.readFile(destFilePath, "utf-8");
706
741
  const sourceContent = await fs4.readFile(sourceFilePath, "utf-8");
707
- let newContent = expandContent(sourceContent, {
708
- flags,
709
- baseDir
710
- });
742
+ let newContent = applyMarkdownFixes(
743
+ stripInternalMetadata(
744
+ expandContent(sourceContent, {
745
+ flags,
746
+ baseDir
747
+ })
748
+ )
749
+ );
711
750
  if (metadata && allowedToolsSet) {
712
751
  const commandMetadata = metadata[file];
713
752
  const requestedTools = commandMetadata?.[REQUESTED_TOOLS_KEY] || [];
@@ -760,11 +799,6 @@ async function getCommandsGroupedByCategory() {
760
799
  selectedByDefault: data.selectedByDefault !== false
761
800
  });
762
801
  }
763
- for (const category of Object.keys(grouped)) {
764
- if (!CATEGORY_ORDER.includes(category)) {
765
- throw new Error(`Unknown category: ${category}`);
766
- }
767
- }
768
802
  for (const category of Object.keys(grouped)) {
769
803
  grouped[category].sort((a, b) => {
770
804
  const orderA = metadata[a.value].order;
@@ -781,10 +815,6 @@ async function getCommandsGroupedByCategory() {
781
815
  }
782
816
  return sortedGrouped;
783
817
  }
784
- function extractLabelFromTool(tool) {
785
- const match = tool.match(/^Bash\(([^:]+):/);
786
- return match ? match[1] : tool;
787
- }
788
818
  function formatCommandsHint(commands) {
789
819
  if (commands.length <= 2) {
790
820
  return commands.map((c) => `/${c}`).join(", ");
@@ -808,7 +838,7 @@ async function getRequestedToolsOptions() {
808
838
  }
809
839
  return Array.from(toolToCommands.entries()).map(([tool, commands]) => ({
810
840
  value: tool,
811
- label: extractLabelFromTool(tool),
841
+ label: tool,
812
842
  hint: formatCommandsHint(commands)
813
843
  }));
814
844
  }
@@ -824,6 +854,37 @@ function getDestinationPath(outputPath, scope) {
824
854
  }
825
855
  throw new Error("Either outputPath or scope must be provided");
826
856
  }
857
+ function stripInternalMetadata(content) {
858
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
859
+ if (!frontmatterMatch) {
860
+ return content;
861
+ }
862
+ const frontmatter = frontmatterMatch[1];
863
+ const lines = frontmatter.split("\n");
864
+ const filteredLines = [];
865
+ let skipMultiline = false;
866
+ for (const line of lines) {
867
+ if (/^_[\w-]+:/.test(line)) {
868
+ skipMultiline = line.endsWith(":") || /^_[\w-]+:\s*$/.test(line);
869
+ continue;
870
+ }
871
+ if (skipMultiline && /^\s+/.test(line)) {
872
+ continue;
873
+ }
874
+ skipMultiline = false;
875
+ filteredLines.push(line);
876
+ }
877
+ const newFrontmatter = filteredLines.join("\n");
878
+ return content.replace(/^---\n[\s\S]*?\n---/, `---
879
+ ${newFrontmatter}
880
+ ---`);
881
+ }
882
+ function applyMarkdownFixes(content) {
883
+ const results = lint({
884
+ strings: { content }
885
+ });
886
+ return applyFixes(content, results.content);
887
+ }
827
888
  function extractTemplateBlocks(content) {
828
889
  const blocks = [];
829
890
  const withCommandsRegex = /<claude-commands-template\s+commands="([^"]+)">([\s\S]*?)<\/claude-commands-template>/g;
@@ -845,9 +906,7 @@ async function generateToDirectory(outputPath, scope, options) {
845
906
  const destinationPath = getDestinationPath(outputPath, scope);
846
907
  const sourcePath = path4.join(__dirname3, "..", DIRECTORIES.SOURCES);
847
908
  const flags = options?.flags ?? [];
848
- const allFiles = (await fs4.readdir(sourcePath)).filter(
849
- (f) => f.endsWith(".md")
850
- );
909
+ const allFiles = await getSourceFiles(options?.includeContribCommands);
851
910
  let files = options?.commands ? allFiles.filter((f) => options.commands.includes(f)) : allFiles;
852
911
  if (options?.skipFiles) {
853
912
  const prefix2 = options?.commandPrefix || "";
@@ -863,9 +922,13 @@ async function generateToDirectory(outputPath, scope, options) {
863
922
  flags,
864
923
  baseDir
865
924
  });
925
+ const cleanedContent = applyMarkdownFixes(
926
+ stripInternalMetadata(expandedContent)
927
+ );
928
+ const outputFileName = stripContribPrefix(file);
866
929
  await fs4.writeFile(
867
- path4.join(destinationPath, prefix + file),
868
- expandedContent
930
+ path4.join(destinationPath, prefix + outputFileName),
931
+ cleanedContent
869
932
  );
870
933
  }
871
934
  if (options?.allowedTools && options.allowedTools.length > 0) {
@@ -878,7 +941,8 @@ async function generateToDirectory(outputPath, scope, options) {
878
941
  (tool) => allowedToolsSet.has(tool)
879
942
  );
880
943
  if (toolsForCommand.length > 0) {
881
- const filePath = path4.join(destinationPath, prefix + file);
944
+ const outputFileName = stripContribPrefix(file);
945
+ const filePath = path4.join(destinationPath, prefix + outputFileName);
882
946
  const content = await fs4.readFile(filePath, "utf-8");
883
947
  const allowedToolsYaml = `allowed-tools: ${toolsForCommand.join(", ")}`;
884
948
  const modifiedContent = content.replace(
@@ -906,8 +970,9 @@ ${allowedToolsYaml}
906
970
  const templates = extractTemplateBlocks(sourceContent);
907
971
  if (templates.length > 0) {
908
972
  for (const file of files) {
909
- const commandName = path4.basename(file, ".md");
910
- const actualFileName = options?.commandPrefix ? options.commandPrefix + file : file;
973
+ const outputFileName = stripContribPrefix(file);
974
+ const commandName = path4.basename(outputFileName, ".md");
975
+ const actualFileName = options?.commandPrefix ? options.commandPrefix + outputFileName : outputFileName;
911
976
  const filePath = path4.join(destinationPath, actualFileName);
912
977
  let content = await fs4.readFile(filePath, "utf-8");
913
978
  let modified = false;
@@ -919,7 +984,7 @@ ${allowedToolsYaml}
919
984
  modified = true;
920
985
  }
921
986
  if (modified) {
922
- await fs4.writeFile(filePath, content);
987
+ await fs4.writeFile(filePath, applyMarkdownFixes(content));
923
988
  }
924
989
  }
925
990
  templateInjected = true;
@@ -943,6 +1008,10 @@ function isInteractiveTTY() {
943
1008
 
944
1009
  // scripts/cli.ts
945
1010
  var pc = process.env.FORCE_COLOR ? import_picocolors.default.createColors(true) : import_picocolors.default;
1011
+ var ScopeValues = Object.values(SCOPES);
1012
+ var ScopeSchema = v2.picklist(ScopeValues);
1013
+ var FlagValues = FLAG_OPTIONS.map((f) => f.value);
1014
+ var FlagsSchema = v2.array(v2.picklist(FlagValues));
946
1015
  function splitChangeIntoLines(value) {
947
1016
  const lines = value.split("\n");
948
1017
  if (lines[lines.length - 1] === "") lines.pop();
@@ -1067,16 +1136,16 @@ async function main(args) {
1067
1136
  let selectedFlags;
1068
1137
  let cachedExistingFiles;
1069
1138
  if (args?.scope) {
1070
- scope = args.scope;
1139
+ scope = v2.parse(ScopeSchema, args.scope);
1071
1140
  commandPrefix = args.prefix ?? "";
1072
1141
  selectedCommands = args.commands;
1073
- selectedFlags = args.flags;
1142
+ selectedFlags = args.flags ? v2.parse(FlagsSchema, args.flags) : void 0;
1074
1143
  if (args.updateExisting) {
1075
- cachedExistingFiles = await checkExistingFiles(
1076
- void 0,
1077
- scope,
1078
- { commandPrefix: commandPrefix || "", flags: selectedFlags }
1079
- );
1144
+ cachedExistingFiles = await checkExistingFiles(void 0, scope, {
1145
+ commandPrefix: commandPrefix || "",
1146
+ flags: selectedFlags,
1147
+ includeContribCommands: args.includeContribCommands
1148
+ });
1080
1149
  selectedCommands = cachedExistingFiles.map((f) => f.filename);
1081
1150
  if (selectedCommands.length === 0) {
1082
1151
  log.warn("No existing commands found in target directory");
@@ -1107,10 +1176,15 @@ async function main(args) {
1107
1176
  if (isCancel(commandPrefix)) {
1108
1177
  return;
1109
1178
  }
1110
- const flagOptions = getFlagsGroupedByCategory();
1111
1179
  selectedFlags = await groupMultiselect({
1112
1180
  message: "Select feature flags (optional)",
1113
- options: flagOptions,
1181
+ options: {
1182
+ "Feature Flags": FLAG_OPTIONS.map(({ value, label, hint }) => ({
1183
+ value,
1184
+ label,
1185
+ hint
1186
+ }))
1187
+ },
1114
1188
  required: false
1115
1189
  });
1116
1190
  if (isCancel(selectedFlags)) {
@@ -1123,7 +1197,8 @@ async function main(args) {
1123
1197
  scope,
1124
1198
  {
1125
1199
  commandPrefix: commandPrefix || "",
1126
- flags: selectedFlags
1200
+ flags: selectedFlags,
1201
+ includeContribCommands: args.includeContribCommands
1127
1202
  }
1128
1203
  );
1129
1204
  const existingFilenames = new Set(
@@ -1171,18 +1246,17 @@ async function main(args) {
1171
1246
  commandPrefix,
1172
1247
  commands: selectedCommands,
1173
1248
  allowedTools: selectedAllowedTools,
1174
- flags: selectedFlags
1249
+ flags: selectedFlags,
1250
+ includeContribCommands: args?.includeContribCommands
1175
1251
  });
1176
1252
  const skipFiles = [];
1253
+ const conflictingFiles = existingFiles.filter((f) => !f.isIdentical);
1177
1254
  const shouldSkipConflicts = args?.skipOnConflict || !isInteractiveTTY();
1178
1255
  if (args?.overwrite) {
1179
- for (const file of existingFiles) {
1180
- if (!file.isIdentical) {
1181
- log.info(`Overwriting ${file.filename}`);
1182
- }
1256
+ for (const file of conflictingFiles) {
1257
+ log.info(`Overwriting ${file.filename}`);
1183
1258
  }
1184
1259
  } else if (!shouldSkipConflicts) {
1185
- const conflictingFiles = existingFiles.filter((f) => !f.isIdentical);
1186
1260
  const hasMultipleConflicts = conflictingFiles.length > 1;
1187
1261
  let overwriteAllSelected = false;
1188
1262
  let skipAllSelected = false;
@@ -1236,14 +1310,12 @@ async function main(args) {
1236
1310
  }
1237
1311
  }
1238
1312
  }
1239
- } else if (shouldSkipConflicts) {
1240
- for (const file of existingFiles) {
1241
- if (!file.isIdentical) {
1242
- skipFiles.push(file.filename);
1243
- log.warn(`Skipping ${file.filename} (conflict)`);
1244
- }
1313
+ } else {
1314
+ for (const file of conflictingFiles) {
1315
+ skipFiles.push(file.filename);
1316
+ log.warn(`Skipping ${file.filename} (conflict)`);
1245
1317
  }
1246
- if (skipFiles.length > 0 && !isInteractiveTTY()) {
1318
+ if (conflictingFiles.length > 0 && !isInteractiveTTY()) {
1247
1319
  log.info(
1248
1320
  "To resolve conflicts, run interactively or use --overwrite to overwrite"
1249
1321
  );
@@ -1255,7 +1327,8 @@ async function main(args) {
1255
1327
  commands: selectedCommands,
1256
1328
  skipFiles,
1257
1329
  allowedTools: selectedAllowedTools,
1258
- flags: selectedFlags
1330
+ flags: selectedFlags,
1331
+ includeContribCommands: args?.includeContribCommands
1259
1332
  });
1260
1333
  const fullPath = scope === "project" ? `${process.cwd()}/.claude/commands` : `${os2.homedir()}/.claude/commands`;
1261
1334
  outro(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbern/claude-instructions",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "TDD workflow commands for Claude Code CLI",
5
5
  "type": "module",
6
6
  "bin": "./bin/cli.js",
@@ -27,7 +27,9 @@
27
27
  "author": "wbern",
28
28
  "license": "MIT",
29
29
  "scripts": {
30
- "build": "tsx scripts/build.ts",
30
+ "build": "pnpm build:readme && pnpm build:commands && pnpm exec markdownlint --fix .claude/commands/*.md",
31
+ "build:readme": "tsx scripts/build.ts",
32
+ "build:commands": "pnpm build:cli && node bin/cli.js --scope=project --flags=beads,no-plan-files --include-contrib-commands --overwrite",
31
33
  "build:cli": "tsup",
32
34
  "test:manual": "pnpm build:cli && TMPDIR=$(mktemp -d) && pnpm pack --pack-destination $TMPDIR && cd $TMPDIR && tar -xzf *.tgz && cd package && pnpm i && node bin/cli.js",
33
35
  "test:quick-manual": "pnpm build:cli && node bin/cli.js",
@@ -52,7 +54,6 @@
52
54
  "jscpd": "^4.0.5",
53
55
  "knip": "^5.70.2",
54
56
  "lint-staged": "^16.2.7",
55
- "markdownlint": "^0.40.0",
56
57
  "markdownlint-cli": "^0.46.0",
57
58
  "picocolors": "^1.1.1",
58
59
  "prettier": "^3.7.2",
@@ -64,7 +65,9 @@
64
65
  },
65
66
  "dependencies": {
66
67
  "@clack/prompts": "^0.11.0",
67
- "fs-extra": "^11.3.2"
68
+ "fs-extra": "^11.3.2",
69
+ "markdownlint": "^0.40.0",
70
+ "valibot": "^1.2.0"
68
71
  },
69
72
  "release": {
70
73
  "branches": [
package/src/README.md CHANGED
@@ -43,7 +43,7 @@ pnpm dlx @wbern/claude-instructions
43
43
 
44
44
  The interactive installer lets you choose:
45
45
 
46
- - **Variant**: With or without [Beads MCP](https://github.com/steveyegge/beads) integration
46
+ - **Feature flags**: Enable optional integrations like [Beads MCP](https://github.com/steveyegge/beads)
47
47
  - **Scope**: User-level (global) or project-level installation
48
48
 
49
49
  After installation, restart Claude Code if it's currently running.
@@ -61,7 +61,7 @@ Then add a postinstall script to your `package.json`:
61
61
  ```json
62
62
  {
63
63
  "scripts": {
64
- "postinstall": "npx @wbern/claude-instructions --variant=without-beads --scope=project --prefix="
64
+ "postinstall": "npx @wbern/claude-instructions --scope=project --overwrite"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@wbern/claude-instructions": "^1.0.0"
@@ -0,0 +1,3 @@
1
+ ## Plan File Restriction
2
+
3
+ **NEVER create, read, or update plan.md files.** Claude Code's internal planning files are disabled for this project. Use other methods to track implementation progress (e.g., comments, todo lists, or external tools).
@@ -0,0 +1,137 @@
1
+ ---
2
+ description: Create a new slash command for this repository
3
+ argument-hint: <command-name> <command-info>
4
+ _hint: Create command
5
+ _category: Utilities
6
+ _order: 99
7
+ ---
8
+
9
+ <!-- docs INCLUDE path='src/fragments/universal-guidelines.md' -->
10
+ <!-- /docs -->
11
+
12
+ Create a new custom command in `src/sources/` following the patterns below. Assess the structure carefully using the below info but also researching the repo.
13
+
14
+ Command to create: $ARGUMENTS
15
+
16
+ ## File Structure
17
+
18
+ Create `src/sources/<command-name>.md` with:
19
+ 1. Frontmatter (required fields below)
20
+ 2. INCLUDE directives for shared content
21
+ - We always include the `docs INCLUDE path='src/fragments/universal-guidelines.md'` fragment
22
+ 3. Command-specific content
23
+ 4. Exactly ONE `[DOLLAR]ARGUMENTS` placeholder
24
+
25
+ After creating, run `pnpm build` and `pnpm vitest run -u` to update snapshots.
26
+
27
+ ## Frontmatter Template
28
+
29
+ ```yaml
30
+ ---
31
+ description: Brief description for /help
32
+ argument-hint: [optional-arg] or <required-arg> or (no arguments - interactive)
33
+ _hint: Short 2-3 word hint
34
+ _category: Test-Driven Development | Planning | Workflow | Ship / Show / Ask | Utilities | [Something else]
35
+ _order: 1-99
36
+ ---
37
+ ```
38
+
39
+ Optional: `_requested-tools` (array), `_selectedByDefault: false`
40
+
41
+ ## Category Patterns
42
+
43
+ ### Test-Driven Development (spike, red, green, refactor, cycle)
44
+
45
+ ```markdown
46
+ [PHASE] PHASE! Apply the below to the info given by user input here:
47
+
48
+ [DOLLAR]ARGUMENTS
49
+
50
+ < !-- docs INCLUDE path='src/fragments/universal-guidelines.md' -->
51
+ < !-- /docs -->
52
+
53
+ < !-- docs INCLUDE path='src/fragments/beads-awareness.md' featureFlag='beads' -->
54
+ < !-- /docs -->
55
+
56
+ < !-- docs INCLUDE path='src/fragments/fallback-arguments-beads.md' featureFlag='beads' elsePath='src/fragments/fallback-arguments.md' -->
57
+ < !-- /docs -->
58
+
59
+ < !-- docs INCLUDE path='src/fragments/tdd-fundamentals.md' -->
60
+ < !-- /docs -->
61
+ ```
62
+
63
+ Add for refactor: `peeping-tom-warning.md`, `consistency-check.md`
64
+ Add for red: `aaa-pattern.md`
65
+
66
+ ### Planning (issue, plan)
67
+
68
+ ```markdown
69
+ # [Title]
70
+
71
+ < !-- docs INCLUDE path='src/fragments/universal-guidelines.md' -->
72
+ < !-- /docs -->
73
+
74
+ < !-- docs INCLUDE path='src/fragments/beads-awareness.md' featureFlag='beads' -->
75
+ < !-- /docs -->
76
+
77
+ [Description and [DOLLAR] embedded in flow]
78
+
79
+ < !-- docs INCLUDE path='src/fragments/discovery-phase.md' -->
80
+ < !-- /docs -->
81
+
82
+ < !-- docs INCLUDE path='src/fragments/beads-integration.md' featureFlag='beads' -->
83
+ < !-- /docs -->
84
+ ```
85
+
86
+ ### Workflow (commit, pr, gap, code-review)
87
+
88
+ ```markdown
89
+ < !-- docs INCLUDE path='src/fragments/universal-guidelines.md' -->
90
+ < !-- /docs -->
91
+
92
+ < !-- docs INCLUDE path='src/fragments/beads-awareness.md' featureFlag='beads' -->
93
+ < !-- /docs -->
94
+
95
+ [Workflow description]
96
+
97
+ [DOLLAR]
98
+
99
+ [Process steps]
100
+ ```
101
+
102
+ commit: add `commit-process.md`, `no-plan-files.md` (with flag)
103
+ pr/gap: add `beads-integration.md` at end
104
+ code-review: add `_requested-tools` for git commands
105
+
106
+ ### Ship / Show / Ask (ship, show, ask)
107
+
108
+ Use `_selectedByDefault: false`. Include prerequisites, safety checks, and reference other S/S/A commands.
109
+
110
+ ### Utilities (add-command, kata, tdd-review)
111
+
112
+ Flexible structure. Interactive commands use `(no arguments - interactive)` hint.
113
+
114
+ ## Available Fragments
115
+
116
+ | Fragment | Use For |
117
+ |----------|---------|
118
+ | `universal-guidelines.md` | Always first |
119
+ | `beads-awareness.md` | Always second (featureFlag='beads') |
120
+ | `tdd-fundamentals.md` | TDD commands |
121
+ | `fallback-arguments-beads.md` | TDD fallback (featureFlag='beads', elsePath to fallback-arguments.md) |
122
+ | `aaa-pattern.md` | Red phase |
123
+ | `peeping-tom-warning.md` | Refactor phase |
124
+ | `consistency-check.md` | Refactor, gap |
125
+ | `discovery-phase.md` | Planning |
126
+ | `beads-integration.md` | PR, planning (featureFlag='beads') |
127
+ | `commit-process.md` | Commit |
128
+ | `no-plan-files.md` | Commit (featureFlag='no-plan-files') |
129
+ | `github-issue-fetch.md` | Issue fetching |
130
+ | `test-quality-criteria.md` | Code review |
131
+
132
+ ## Rules
133
+
134
+ 1. `[DOLLAR]` - exactly once per source, never in fragments
135
+ 2. Fragments must not include other fragments
136
+ 3. Remove space after `<` in real INCLUDE directives (shown escaped above)
137
+ 4. Underscore-prefixed metadata stripped from output
@@ -12,6 +12,9 @@ _order: 1
12
12
  <!-- docs INCLUDE path='src/fragments/beads-awareness.md' featureFlag='beads' -->
13
13
  <!-- /docs -->
14
14
 
15
+ <!-- docs INCLUDE path='src/fragments/no-plan-files.md' featureFlag='no-plan-files' -->
16
+ <!-- /docs -->
17
+
15
18
  Create a git commit following project standards
16
19
 
17
20
  Include any of the following info if specified: $ARGUMENTS