@wbern/claude-instructions 2.0.0 → 2.1.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/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"));
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();
@@ -528,13 +542,6 @@ function generateHelpText() {
528
542
  return lines.join("\n");
529
543
  }
530
544
 
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
545
  // scripts/generate-readme.ts
539
546
  var __filename2 = fileURLToPath2(import.meta.url);
540
547
  var __dirname2 = path3.dirname(__filename2);
@@ -549,6 +556,15 @@ var CATEGORIES = {
549
556
  WORKTREE: "Worktree Management",
550
557
  UTILITIES: "Utilities"
551
558
  };
559
+ var CategoryValues = Object.values(CATEGORIES);
560
+ var CategorySchema = v.picklist(CategoryValues);
561
+ var RequiredFrontmatterSchema = v.object({
562
+ description: v.pipe(v.string(), v.minLength(1)),
563
+ _order: v.number()
564
+ });
565
+ var IncludeOptionsSchema = v.object({
566
+ path: v.string()
567
+ });
552
568
  function parseFrontmatter(content) {
553
569
  const match = content.match(FRONTMATTER_REGEX);
554
570
  if (!match) return {};
@@ -591,7 +607,8 @@ function parseFrontmatter(content) {
591
607
  return frontmatter;
592
608
  }
593
609
  function getCategory(frontmatter) {
594
- return frontmatter._category || CATEGORIES.UTILITIES;
610
+ const category = frontmatter._category || CATEGORIES.UTILITIES;
611
+ return v.parse(CategorySchema, category);
595
612
  }
596
613
  function generateCommandsMetadata() {
597
614
  const sourcesDir = path3.join(PROJECT_ROOT, SOURCES_DIR);
@@ -600,12 +617,13 @@ function generateCommandsMetadata() {
600
617
  for (const file of files) {
601
618
  const content = fs3.readFileSync(path3.join(sourcesDir, file), "utf8");
602
619
  const frontmatter = parseFrontmatter(content);
620
+ const validated = v.parse(RequiredFrontmatterSchema, frontmatter);
603
621
  const requestedTools = frontmatter[REQUESTED_TOOLS_KEY];
604
622
  metadata[file] = {
605
- description: frontmatter.description || "No description",
623
+ description: validated.description,
606
624
  hint: frontmatter._hint,
607
625
  category: getCategory(frontmatter),
608
- order: typeof frontmatter._order === "number" ? frontmatter._order : 999,
626
+ order: validated._order,
609
627
  ...frontmatter._selectedByDefault === false ? { selectedByDefault: false } : {},
610
628
  ...requestedTools ? { [REQUESTED_TOOLS_KEY]: requestedTools } : {}
611
629
  };
@@ -614,6 +632,8 @@ function generateCommandsMetadata() {
614
632
  }
615
633
 
616
634
  // scripts/cli-generator.ts
635
+ import { lint } from "markdownlint/sync";
636
+ import { applyFixes } from "markdownlint";
617
637
  var __filename3 = fileURLToPath3(import.meta.url);
618
638
  var __dirname3 = path4.dirname(__filename3);
619
639
  var SCOPES = {
@@ -645,19 +665,14 @@ var FLAG_OPTIONS = [
645
665
  label: "Beads MCP",
646
666
  hint: "Local issue tracking",
647
667
  category: "Feature Flags"
668
+ },
669
+ {
670
+ value: "no-plan-files",
671
+ label: "No Plan Files",
672
+ hint: "Forbid Claude Code's internal plan.md",
673
+ category: "Feature Flags"
648
674
  }
649
675
  ];
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
676
  function getScopeOptions(terminalWidth = 80) {
662
677
  const projectPath = path4.join(
663
678
  process.cwd(),
@@ -684,7 +699,7 @@ function getScopeOptions(terminalWidth = 80) {
684
699
  }
685
700
  async function checkExistingFiles(outputPath, scope, options) {
686
701
  const sourcePath = path4.join(__dirname3, "..", DIRECTORIES.SOURCES);
687
- const destinationPath = outputPath || getDestinationPath(outputPath, scope);
702
+ const destinationPath = getDestinationPath(outputPath, scope);
688
703
  const flags = options?.flags ?? [];
689
704
  const allFiles = await fs4.readdir(sourcePath);
690
705
  const files = options?.commands ? allFiles.filter((f) => options.commands.includes(f)) : allFiles;
@@ -704,10 +719,14 @@ async function checkExistingFiles(outputPath, scope, options) {
704
719
  if (await fs4.pathExists(destFilePath)) {
705
720
  const existingContent = await fs4.readFile(destFilePath, "utf-8");
706
721
  const sourceContent = await fs4.readFile(sourceFilePath, "utf-8");
707
- let newContent = expandContent(sourceContent, {
708
- flags,
709
- baseDir
710
- });
722
+ let newContent = applyMarkdownFixes(
723
+ stripInternalMetadata(
724
+ expandContent(sourceContent, {
725
+ flags,
726
+ baseDir
727
+ })
728
+ )
729
+ );
711
730
  if (metadata && allowedToolsSet) {
712
731
  const commandMetadata = metadata[file];
713
732
  const requestedTools = commandMetadata?.[REQUESTED_TOOLS_KEY] || [];
@@ -760,11 +779,6 @@ async function getCommandsGroupedByCategory() {
760
779
  selectedByDefault: data.selectedByDefault !== false
761
780
  });
762
781
  }
763
- for (const category of Object.keys(grouped)) {
764
- if (!CATEGORY_ORDER.includes(category)) {
765
- throw new Error(`Unknown category: ${category}`);
766
- }
767
- }
768
782
  for (const category of Object.keys(grouped)) {
769
783
  grouped[category].sort((a, b) => {
770
784
  const orderA = metadata[a.value].order;
@@ -781,10 +795,6 @@ async function getCommandsGroupedByCategory() {
781
795
  }
782
796
  return sortedGrouped;
783
797
  }
784
- function extractLabelFromTool(tool) {
785
- const match = tool.match(/^Bash\(([^:]+):/);
786
- return match ? match[1] : tool;
787
- }
788
798
  function formatCommandsHint(commands) {
789
799
  if (commands.length <= 2) {
790
800
  return commands.map((c) => `/${c}`).join(", ");
@@ -808,7 +818,7 @@ async function getRequestedToolsOptions() {
808
818
  }
809
819
  return Array.from(toolToCommands.entries()).map(([tool, commands]) => ({
810
820
  value: tool,
811
- label: extractLabelFromTool(tool),
821
+ label: tool,
812
822
  hint: formatCommandsHint(commands)
813
823
  }));
814
824
  }
@@ -824,6 +834,37 @@ function getDestinationPath(outputPath, scope) {
824
834
  }
825
835
  throw new Error("Either outputPath or scope must be provided");
826
836
  }
837
+ function stripInternalMetadata(content) {
838
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
839
+ if (!frontmatterMatch) {
840
+ return content;
841
+ }
842
+ const frontmatter = frontmatterMatch[1];
843
+ const lines = frontmatter.split("\n");
844
+ const filteredLines = [];
845
+ let skipMultiline = false;
846
+ for (const line of lines) {
847
+ if (/^_[\w-]+:/.test(line)) {
848
+ skipMultiline = line.endsWith(":") || /^_[\w-]+:\s*$/.test(line);
849
+ continue;
850
+ }
851
+ if (skipMultiline && /^\s+/.test(line)) {
852
+ continue;
853
+ }
854
+ skipMultiline = false;
855
+ filteredLines.push(line);
856
+ }
857
+ const newFrontmatter = filteredLines.join("\n");
858
+ return content.replace(/^---\n[\s\S]*?\n---/, `---
859
+ ${newFrontmatter}
860
+ ---`);
861
+ }
862
+ function applyMarkdownFixes(content) {
863
+ const results = lint({
864
+ strings: { content }
865
+ });
866
+ return applyFixes(content, results.content);
867
+ }
827
868
  function extractTemplateBlocks(content) {
828
869
  const blocks = [];
829
870
  const withCommandsRegex = /<claude-commands-template\s+commands="([^"]+)">([\s\S]*?)<\/claude-commands-template>/g;
@@ -863,9 +904,12 @@ async function generateToDirectory(outputPath, scope, options) {
863
904
  flags,
864
905
  baseDir
865
906
  });
907
+ const cleanedContent = applyMarkdownFixes(
908
+ stripInternalMetadata(expandedContent)
909
+ );
866
910
  await fs4.writeFile(
867
911
  path4.join(destinationPath, prefix + file),
868
- expandedContent
912
+ cleanedContent
869
913
  );
870
914
  }
871
915
  if (options?.allowedTools && options.allowedTools.length > 0) {
@@ -919,7 +963,7 @@ ${allowedToolsYaml}
919
963
  modified = true;
920
964
  }
921
965
  if (modified) {
922
- await fs4.writeFile(filePath, content);
966
+ await fs4.writeFile(filePath, applyMarkdownFixes(content));
923
967
  }
924
968
  }
925
969
  templateInjected = true;
@@ -943,6 +987,10 @@ function isInteractiveTTY() {
943
987
 
944
988
  // scripts/cli.ts
945
989
  var pc = process.env.FORCE_COLOR ? import_picocolors.default.createColors(true) : import_picocolors.default;
990
+ var ScopeValues = Object.values(SCOPES);
991
+ var ScopeSchema = v2.picklist(ScopeValues);
992
+ var FlagValues = FLAG_OPTIONS.map((f) => f.value);
993
+ var FlagsSchema = v2.array(v2.picklist(FlagValues));
946
994
  function splitChangeIntoLines(value) {
947
995
  const lines = value.split("\n");
948
996
  if (lines[lines.length - 1] === "") lines.pop();
@@ -1067,16 +1115,15 @@ async function main(args) {
1067
1115
  let selectedFlags;
1068
1116
  let cachedExistingFiles;
1069
1117
  if (args?.scope) {
1070
- scope = args.scope;
1118
+ scope = v2.parse(ScopeSchema, args.scope);
1071
1119
  commandPrefix = args.prefix ?? "";
1072
1120
  selectedCommands = args.commands;
1073
- selectedFlags = args.flags;
1121
+ selectedFlags = args.flags ? v2.parse(FlagsSchema, args.flags) : void 0;
1074
1122
  if (args.updateExisting) {
1075
- cachedExistingFiles = await checkExistingFiles(
1076
- void 0,
1077
- scope,
1078
- { commandPrefix: commandPrefix || "", flags: selectedFlags }
1079
- );
1123
+ cachedExistingFiles = await checkExistingFiles(void 0, scope, {
1124
+ commandPrefix: commandPrefix || "",
1125
+ flags: selectedFlags
1126
+ });
1080
1127
  selectedCommands = cachedExistingFiles.map((f) => f.filename);
1081
1128
  if (selectedCommands.length === 0) {
1082
1129
  log.warn("No existing commands found in target directory");
@@ -1107,10 +1154,15 @@ async function main(args) {
1107
1154
  if (isCancel(commandPrefix)) {
1108
1155
  return;
1109
1156
  }
1110
- const flagOptions = getFlagsGroupedByCategory();
1111
1157
  selectedFlags = await groupMultiselect({
1112
1158
  message: "Select feature flags (optional)",
1113
- options: flagOptions,
1159
+ options: {
1160
+ "Feature Flags": FLAG_OPTIONS.map(({ value, label, hint }) => ({
1161
+ value,
1162
+ label,
1163
+ hint
1164
+ }))
1165
+ },
1114
1166
  required: false
1115
1167
  });
1116
1168
  if (isCancel(selectedFlags)) {
@@ -1174,15 +1226,13 @@ async function main(args) {
1174
1226
  flags: selectedFlags
1175
1227
  });
1176
1228
  const skipFiles = [];
1229
+ const conflictingFiles = existingFiles.filter((f) => !f.isIdentical);
1177
1230
  const shouldSkipConflicts = args?.skipOnConflict || !isInteractiveTTY();
1178
1231
  if (args?.overwrite) {
1179
- for (const file of existingFiles) {
1180
- if (!file.isIdentical) {
1181
- log.info(`Overwriting ${file.filename}`);
1182
- }
1232
+ for (const file of conflictingFiles) {
1233
+ log.info(`Overwriting ${file.filename}`);
1183
1234
  }
1184
1235
  } else if (!shouldSkipConflicts) {
1185
- const conflictingFiles = existingFiles.filter((f) => !f.isIdentical);
1186
1236
  const hasMultipleConflicts = conflictingFiles.length > 1;
1187
1237
  let overwriteAllSelected = false;
1188
1238
  let skipAllSelected = false;
@@ -1236,14 +1286,12 @@ async function main(args) {
1236
1286
  }
1237
1287
  }
1238
1288
  }
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
- }
1289
+ } else {
1290
+ for (const file of conflictingFiles) {
1291
+ skipFiles.push(file.filename);
1292
+ log.warn(`Skipping ${file.filename} (conflict)`);
1245
1293
  }
1246
- if (skipFiles.length > 0 && !isInteractiveTTY()) {
1294
+ if (conflictingFiles.length > 0 && !isInteractiveTTY()) {
1247
1295
  log.info(
1248
1296
  "To resolve conflicts, run interactively or use --overwrite to overwrite"
1249
1297
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbern/claude-instructions",
3
- "version": "2.0.0",
3
+ "version": "2.1.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 --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": [
@@ -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).
@@ -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