@wbern/claude-instructions 1.21.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.
Files changed (80) hide show
  1. package/README.md +4 -2
  2. package/bin/cli.js +414 -229
  3. package/package.json +8 -6
  4. package/src/README.md +279 -0
  5. package/src/fragments/aaa-pattern.md +7 -0
  6. package/src/fragments/beads-awareness.md +1 -0
  7. package/src/fragments/beads-integration.md +8 -0
  8. package/{downloads/without-beads/commit.md → src/fragments/commit-process.md} +0 -17
  9. package/src/fragments/consistency-check.md +1 -0
  10. package/src/fragments/discovery-phase.md +22 -0
  11. package/src/fragments/fallback-arguments-beads.md +3 -0
  12. package/src/fragments/fallback-arguments.md +1 -0
  13. package/src/fragments/fullwidth-dollar-note.md +1 -0
  14. package/src/fragments/gap-beads.md +1 -0
  15. package/src/fragments/git-host-detection.md +19 -0
  16. package/src/fragments/github-issue-fetch.md +10 -0
  17. package/src/fragments/no-plan-files.md +3 -0
  18. package/src/fragments/peeping-tom-warning.md +9 -0
  19. package/src/fragments/plan-beads-context-hint.md +1 -0
  20. package/src/fragments/plan-beads-details.md +49 -0
  21. package/src/fragments/plan-beads-integration.md +2 -0
  22. package/src/fragments/summarize-beads.md +8 -0
  23. package/{downloads/without-beads/summarize.md → src/fragments/summarize-structure.md} +0 -20
  24. package/{downloads/without-beads/tdd.md → src/fragments/tdd-fundamentals.md} +0 -21
  25. package/src/fragments/test-quality-criteria.md +24 -0
  26. package/src/fragments/universal-guidelines.md +6 -0
  27. package/{downloads/without-beads → src/sources}/add-command.md +11 -25
  28. package/{downloads/without-beads → src/sources}/ask.md +11 -6
  29. package/{downloads/without-beads → src/sources}/beepboop.md +7 -6
  30. package/{downloads/without-beads → src/sources}/busycommit.md +9 -36
  31. package/{downloads/without-beads → src/sources}/code-review.md +16 -30
  32. package/src/sources/commit.md +23 -0
  33. package/src/sources/cycle.md +23 -0
  34. package/{downloads/without-beads → src/sources}/gap.md +11 -8
  35. package/src/sources/green.md +23 -0
  36. package/src/sources/issue.md +42 -0
  37. package/{downloads/without-beads → src/sources}/kata.md +10 -9
  38. package/{downloads/without-beads → src/sources}/plan.md +18 -39
  39. package/{downloads/without-beads → src/sources}/pr.md +10 -6
  40. package/src/sources/red.md +26 -0
  41. package/src/sources/refactor.md +27 -0
  42. package/{downloads/without-beads → src/sources}/ship.md +11 -6
  43. package/{downloads/without-beads → src/sources}/show.md +11 -6
  44. package/src/sources/spike.md +23 -0
  45. package/src/sources/summarize.md +23 -0
  46. package/{downloads/without-beads → src/sources}/tdd-review.md +11 -31
  47. package/src/sources/tdd.md +24 -0
  48. package/{downloads/without-beads → src/sources}/worktree-add.md +13 -31
  49. package/{downloads/without-beads → src/sources}/worktree-cleanup.md +9 -27
  50. package/downloads/with-beads/add-command.md +0 -159
  51. package/downloads/with-beads/ask.md +0 -144
  52. package/downloads/with-beads/beepboop.md +0 -47
  53. package/downloads/with-beads/busycommit.md +0 -78
  54. package/downloads/with-beads/code-review.md +0 -263
  55. package/downloads/with-beads/commands-metadata.json +0 -155
  56. package/downloads/with-beads/commit.md +0 -49
  57. package/downloads/with-beads/cycle.md +0 -95
  58. package/downloads/with-beads/gap.md +0 -38
  59. package/downloads/with-beads/green.md +0 -95
  60. package/downloads/with-beads/issue.md +0 -152
  61. package/downloads/with-beads/kata.md +0 -444
  62. package/downloads/with-beads/plan.md +0 -186
  63. package/downloads/with-beads/pr.md +0 -82
  64. package/downloads/with-beads/red.md +0 -103
  65. package/downloads/with-beads/refactor.md +0 -105
  66. package/downloads/with-beads/ship.md +0 -98
  67. package/downloads/with-beads/show.md +0 -114
  68. package/downloads/with-beads/spike.md +0 -95
  69. package/downloads/with-beads/summarize.md +0 -54
  70. package/downloads/with-beads/tdd-review.md +0 -102
  71. package/downloads/with-beads/tdd.md +0 -94
  72. package/downloads/with-beads/worktree-add.md +0 -357
  73. package/downloads/with-beads/worktree-cleanup.md +0 -250
  74. package/downloads/without-beads/commands-metadata.json +0 -155
  75. package/downloads/without-beads/cycle.md +0 -90
  76. package/downloads/without-beads/green.md +0 -90
  77. package/downloads/without-beads/issue.md +0 -140
  78. package/downloads/without-beads/red.md +0 -98
  79. package/downloads/without-beads/refactor.md +0 -100
  80. package/downloads/without-beads/spike.md +0 -90
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;
@@ -234,16 +234,16 @@ var Diff = class {
234
234
  }
235
235
  }
236
236
  }
237
- addToPath(path3, added, removed, oldPosInc, options) {
238
- const last = path3.lastComponent;
237
+ addToPath(path5, added, removed, oldPosInc, options) {
238
+ const last = path5.lastComponent;
239
239
  if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
240
240
  return {
241
- oldPos: path3.oldPos + oldPosInc,
241
+ oldPos: path5.oldPos + oldPosInc,
242
242
  lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
243
243
  };
244
244
  } else {
245
245
  return {
246
- oldPos: path3.oldPos + oldPosInc,
246
+ oldPos: path5.oldPos + oldPosInc,
247
247
  lastComponent: { count: 1, added, removed, previousComponent: last }
248
248
  };
249
249
  }
@@ -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,19 +387,255 @@ 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();
393
- import fs from "fs-extra";
394
+ import fs4 from "fs-extra";
395
+ import path4 from "path";
396
+ import { fileURLToPath as fileURLToPath3 } from "url";
397
+ import os from "os";
398
+
399
+ // scripts/fragment-expander.ts
400
+ init_esm_shims();
401
+ import fs2 from "fs-extra";
394
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
415
+ var TRANSFORM_BLOCK_REGEX = /<!--\s*docs\s+(\w+)([^>]*)-->([\s\S]*?)<!--\s*\/docs\s*-->/g;
416
+ function parseOptions(attrString) {
417
+ const options = {};
418
+ const attrRegex = /(\w+)=['"]([^'"]*)['"]/g;
419
+ let match;
420
+ while ((match = attrRegex.exec(attrString)) !== null) {
421
+ options[match[1]] = match[2];
422
+ }
423
+ return options;
424
+ }
425
+ function expandContent(content, options) {
426
+ const { baseDir, flags } = options;
427
+ return content.replace(
428
+ TRANSFORM_BLOCK_REGEX,
429
+ (_match, transformName, attrString) => {
430
+ if (transformName !== "INCLUDE") {
431
+ throw new Error(`Unknown transform type: ${transformName}`);
432
+ }
433
+ const attrs = parseOptions(attrString);
434
+ const { path: includePath, featureFlag, elsePath } = attrs;
435
+ if (featureFlag && !flags.includes(featureFlag)) {
436
+ if (elsePath) {
437
+ const fullElsePath = path2.join(baseDir, elsePath);
438
+ try {
439
+ return fs2.readFileSync(fullElsePath, "utf8");
440
+ } catch (err) {
441
+ throw new Error(
442
+ `Failed to read elsePath '${elsePath}': ${getErrorMessage(err)}`
443
+ );
444
+ }
445
+ }
446
+ return "";
447
+ }
448
+ if (!includePath) {
449
+ throw new Error("INCLUDE directive missing required 'path' attribute");
450
+ }
451
+ const fullPath = path2.join(baseDir, includePath);
452
+ try {
453
+ return fs2.readFileSync(fullPath, "utf8");
454
+ } catch (err) {
455
+ throw new Error(
456
+ `Failed to read '${includePath}': ${getErrorMessage(err)}`
457
+ );
458
+ }
459
+ }
460
+ );
461
+ }
462
+
463
+ // scripts/generate-readme.ts
464
+ init_esm_shims();
465
+ import fs3 from "fs";
466
+ import path3 from "path";
395
467
  import { fileURLToPath as fileURLToPath2 } from "url";
396
- import os from "os";
468
+ import * as v from "valibot";
469
+
470
+ // scripts/cli-options.ts
471
+ init_esm_shims();
472
+ var CLI_OPTIONS = [
473
+ {
474
+ flag: "--scope",
475
+ key: "scope",
476
+ type: "string",
477
+ description: "Installation scope (project, user)",
478
+ example: "--scope=project",
479
+ requiredForNonInteractive: true
480
+ },
481
+ {
482
+ flag: "--prefix",
483
+ key: "prefix",
484
+ type: "string",
485
+ description: "Add prefix to command names",
486
+ example: "--prefix=my-"
487
+ },
488
+ {
489
+ flag: "--commands",
490
+ key: "commands",
491
+ type: "array",
492
+ description: "Install only specific commands",
493
+ example: "--commands=commit,red,green"
494
+ },
495
+ {
496
+ flag: "--skip-template-injection",
497
+ key: "skipTemplateInjection",
498
+ type: "boolean",
499
+ description: "Skip injecting project CLAUDE.md customizations"
500
+ },
501
+ {
502
+ flag: "--update-existing",
503
+ key: "updateExisting",
504
+ type: "boolean",
505
+ description: "Only update already-installed commands"
506
+ },
507
+ {
508
+ flag: "--overwrite",
509
+ key: "overwrite",
510
+ type: "boolean",
511
+ description: "Overwrite conflicting files without prompting"
512
+ },
513
+ {
514
+ flag: "--skip-on-conflict",
515
+ key: "skipOnConflict",
516
+ type: "boolean",
517
+ description: "Skip conflicting files without prompting"
518
+ },
519
+ {
520
+ flag: "--flags",
521
+ key: "flags",
522
+ type: "array",
523
+ description: "Enable feature flags (beads, github, gitlab, etc.)",
524
+ example: "--flags=beads,github"
525
+ }
526
+ ];
527
+ function generateHelpText() {
528
+ const lines = [
529
+ "Usage: npx @wbern/claude-instructions [options]",
530
+ "",
531
+ "Options:"
532
+ ];
533
+ for (const opt of CLI_OPTIONS) {
534
+ const suffix = opt.type === "string" ? "=<value>" : opt.type === "array" ? "=<list>" : "";
535
+ const padding = 28 - (opt.flag.length + suffix.length);
536
+ lines.push(
537
+ ` ${opt.flag}${suffix}${" ".repeat(Math.max(1, padding))}${opt.description}`
538
+ );
539
+ }
540
+ lines.push(" --help, -h Show this help message");
541
+ lines.push(" --version, -v Show version number");
542
+ return lines.join("\n");
543
+ }
544
+
545
+ // scripts/generate-readme.ts
397
546
  var __filename2 = fileURLToPath2(import.meta.url);
398
- var __dirname2 = path2.dirname(__filename2);
399
- var VARIANTS = {
400
- WITH_BEADS: "with-beads",
401
- WITHOUT_BEADS: "without-beads"
547
+ var __dirname2 = path3.dirname(__filename2);
548
+ var PROJECT_ROOT = path3.resolve(__dirname2, "..");
549
+ var SOURCES_DIR = "src/sources";
550
+ var FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---/;
551
+ var CATEGORIES = {
552
+ PLANNING: "Planning",
553
+ TDD: "Test-Driven Development",
554
+ WORKFLOW: "Workflow",
555
+ SHIP_SHOW_ASK: "Ship / Show / Ask",
556
+ WORKTREE: "Worktree Management",
557
+ UTILITIES: "Utilities"
402
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
+ });
568
+ function parseFrontmatter(content) {
569
+ const match = content.match(FRONTMATTER_REGEX);
570
+ if (!match) return {};
571
+ const frontmatter = {};
572
+ const lines = match[1].split("\n");
573
+ let currentKey = null;
574
+ let currentArray = null;
575
+ for (const line of lines) {
576
+ if (line.match(/^\s+-\s+/) && currentKey && currentArray) {
577
+ const value2 = line.replace(/^\s+-\s+/, "").trim();
578
+ currentArray.push(value2);
579
+ continue;
580
+ }
581
+ if (currentKey && currentArray) {
582
+ frontmatter[currentKey] = currentArray;
583
+ currentKey = null;
584
+ currentArray = null;
585
+ }
586
+ const colonIndex = line.indexOf(":");
587
+ if (colonIndex === -1) continue;
588
+ const key = line.slice(0, colonIndex).trim();
589
+ let value = line.slice(colonIndex + 1).trim();
590
+ if (value === "") {
591
+ currentKey = key;
592
+ currentArray = [];
593
+ continue;
594
+ }
595
+ if (key === "_order" && !isNaN(Number(value))) {
596
+ value = parseInt(value, 10);
597
+ }
598
+ if (key === "_selectedByDefault") {
599
+ frontmatter[key] = value === "true";
600
+ continue;
601
+ }
602
+ frontmatter[key] = value;
603
+ }
604
+ if (currentKey && currentArray) {
605
+ frontmatter[currentKey] = currentArray;
606
+ }
607
+ return frontmatter;
608
+ }
609
+ function getCategory(frontmatter) {
610
+ const category = frontmatter._category || CATEGORIES.UTILITIES;
611
+ return v.parse(CategorySchema, category);
612
+ }
613
+ function generateCommandsMetadata() {
614
+ const sourcesDir = path3.join(PROJECT_ROOT, SOURCES_DIR);
615
+ const files = getMarkdownFiles(sourcesDir);
616
+ const metadata = {};
617
+ for (const file of files) {
618
+ const content = fs3.readFileSync(path3.join(sourcesDir, file), "utf8");
619
+ const frontmatter = parseFrontmatter(content);
620
+ const validated = v.parse(RequiredFrontmatterSchema, frontmatter);
621
+ const requestedTools = frontmatter[REQUESTED_TOOLS_KEY];
622
+ metadata[file] = {
623
+ description: validated.description,
624
+ hint: frontmatter._hint,
625
+ category: getCategory(frontmatter),
626
+ order: validated._order,
627
+ ...frontmatter._selectedByDefault === false ? { selectedByDefault: false } : {},
628
+ ...requestedTools ? { [REQUESTED_TOOLS_KEY]: requestedTools } : {}
629
+ };
630
+ }
631
+ return metadata;
632
+ }
633
+
634
+ // scripts/cli-generator.ts
635
+ import { lint } from "markdownlint/sync";
636
+ import { applyFixes } from "markdownlint";
637
+ var __filename3 = fileURLToPath3(import.meta.url);
638
+ var __dirname3 = path4.dirname(__filename3);
403
639
  var SCOPES = {
404
640
  PROJECT: "project",
405
641
  USER: "user"
@@ -407,7 +643,7 @@ var SCOPES = {
407
643
  var DIRECTORIES = {
408
644
  CLAUDE: ".claude",
409
645
  COMMANDS: "commands",
410
- DOWNLOADS: "downloads"
646
+ SOURCES: "src/sources"
411
647
  };
412
648
  var TEMPLATE_SOURCE_FILES = ["CLAUDE.md", "AGENTS.md"];
413
649
  var REQUESTED_TOOLS_KEY = "_requested-tools";
@@ -423,25 +659,27 @@ function truncatePathFromLeft(pathStr, maxLength) {
423
659
  }
424
660
  return ELLIPSIS + truncated;
425
661
  }
426
- var VARIANT_OPTIONS = [
662
+ var FLAG_OPTIONS = [
427
663
  {
428
- value: VARIANTS.WITH_BEADS,
429
- label: "With Beads",
430
- hint: "Includes Beads task tracking"
664
+ value: "beads",
665
+ label: "Beads MCP",
666
+ hint: "Local issue tracking",
667
+ category: "Feature Flags"
431
668
  },
432
669
  {
433
- value: VARIANTS.WITHOUT_BEADS,
434
- label: "Without Beads",
435
- hint: "Standard commands only"
670
+ value: "no-plan-files",
671
+ label: "No Plan Files",
672
+ hint: "Forbid Claude Code's internal plan.md",
673
+ category: "Feature Flags"
436
674
  }
437
675
  ];
438
676
  function getScopeOptions(terminalWidth = 80) {
439
- const projectPath = path2.join(
677
+ const projectPath = path4.join(
440
678
  process.cwd(),
441
679
  DIRECTORIES.CLAUDE,
442
680
  DIRECTORIES.COMMANDS
443
681
  );
444
- const userPath = path2.join(
682
+ const userPath = path4.join(
445
683
  os.homedir(),
446
684
  DIRECTORIES.CLAUDE,
447
685
  DIRECTORIES.COMMANDS
@@ -459,31 +697,36 @@ function getScopeOptions(terminalWidth = 80) {
459
697
  }
460
698
  ];
461
699
  }
462
- async function checkExistingFiles(outputPath, variant, scope, options) {
463
- const sourcePath = path2.join(
464
- __dirname2,
465
- "..",
466
- DIRECTORIES.DOWNLOADS,
467
- variant || VARIANTS.WITH_BEADS
468
- );
469
- const destinationPath = outputPath || getDestinationPath(outputPath, scope);
470
- const allFiles = await fs.readdir(sourcePath);
700
+ async function checkExistingFiles(outputPath, scope, options) {
701
+ const sourcePath = path4.join(__dirname3, "..", DIRECTORIES.SOURCES);
702
+ const destinationPath = getDestinationPath(outputPath, scope);
703
+ const flags = options?.flags ?? [];
704
+ const allFiles = await fs4.readdir(sourcePath);
471
705
  const files = options?.commands ? allFiles.filter((f) => options.commands.includes(f)) : allFiles;
472
706
  const existingFiles = [];
473
707
  const prefix = options?.commandPrefix || "";
474
708
  let metadata = null;
475
709
  let allowedToolsSet = null;
476
710
  if (options?.allowedTools && options.allowedTools.length > 0) {
477
- metadata = await loadCommandsMetadata(variant || VARIANTS.WITH_BEADS);
711
+ metadata = await loadCommandsMetadata();
478
712
  allowedToolsSet = new Set(options.allowedTools);
479
713
  }
714
+ const baseDir = path4.join(__dirname3, "..");
480
715
  for (const file of files) {
481
716
  const destFileName = prefix + file;
482
- const destFilePath = path2.join(destinationPath, destFileName);
483
- const sourceFilePath = path2.join(sourcePath, file);
484
- if (await fs.pathExists(destFilePath)) {
485
- const existingContent = await fs.readFile(destFilePath, "utf-8");
486
- let newContent = await fs.readFile(sourceFilePath, "utf-8");
717
+ const destFilePath = path4.join(destinationPath, destFileName);
718
+ const sourceFilePath = path4.join(sourcePath, file);
719
+ if (await fs4.pathExists(destFilePath)) {
720
+ const existingContent = await fs4.readFile(destFilePath, "utf-8");
721
+ const sourceContent = await fs4.readFile(sourceFilePath, "utf-8");
722
+ let newContent = applyMarkdownFixes(
723
+ stripInternalMetadata(
724
+ expandContent(sourceContent, {
725
+ flags,
726
+ baseDir
727
+ })
728
+ )
729
+ );
487
730
  if (metadata && allowedToolsSet) {
488
731
  const commandMetadata = metadata[file];
489
732
  const requestedTools = commandMetadata?.[REQUESTED_TOOLS_KEY] || [];
@@ -518,19 +761,11 @@ var CATEGORY_ORDER = [
518
761
  "Utilities",
519
762
  "Ship / Show / Ask"
520
763
  ];
521
- async function loadCommandsMetadata(variant) {
522
- const sourcePath = path2.join(
523
- __dirname2,
524
- "..",
525
- DIRECTORIES.DOWNLOADS,
526
- variant || VARIANTS.WITH_BEADS
527
- );
528
- const metadataPath = path2.join(sourcePath, "commands-metadata.json");
529
- const metadataContent = await fs.readFile(metadataPath, "utf-8");
530
- return JSON.parse(metadataContent);
764
+ async function loadCommandsMetadata() {
765
+ return generateCommandsMetadata();
531
766
  }
532
- async function getCommandsGroupedByCategory(variant) {
533
- const metadata = await loadCommandsMetadata(variant);
767
+ async function getCommandsGroupedByCategory() {
768
+ const metadata = await loadCommandsMetadata();
534
769
  const grouped = {};
535
770
  for (const [filename, data] of Object.entries(metadata)) {
536
771
  const category = data.category;
@@ -544,11 +779,6 @@ async function getCommandsGroupedByCategory(variant) {
544
779
  selectedByDefault: data.selectedByDefault !== false
545
780
  });
546
781
  }
547
- for (const category of Object.keys(grouped)) {
548
- if (!CATEGORY_ORDER.includes(category)) {
549
- throw new Error(`Unknown category: ${category}`);
550
- }
551
- }
552
782
  for (const category of Object.keys(grouped)) {
553
783
  grouped[category].sort((a, b) => {
554
784
  const orderA = metadata[a.value].order;
@@ -565,10 +795,6 @@ async function getCommandsGroupedByCategory(variant) {
565
795
  }
566
796
  return sortedGrouped;
567
797
  }
568
- function extractLabelFromTool(tool) {
569
- const match = tool.match(/^Bash\(([^:]+):/);
570
- return match ? match[1] : tool;
571
- }
572
798
  function formatCommandsHint(commands) {
573
799
  if (commands.length <= 2) {
574
800
  return commands.map((c) => `/${c}`).join(", ");
@@ -577,8 +803,8 @@ function formatCommandsHint(commands) {
577
803
  const remaining = commands.length - 2;
578
804
  return `${first.join(", ")}, and ${remaining} ${remaining === 1 ? "other" : "others"}`;
579
805
  }
580
- async function getRequestedToolsOptions(variant) {
581
- const metadata = await loadCommandsMetadata(variant);
806
+ async function getRequestedToolsOptions() {
807
+ const metadata = await loadCommandsMetadata();
582
808
  const toolToCommands = /* @__PURE__ */ new Map();
583
809
  for (const [filename, data] of Object.entries(metadata)) {
584
810
  if (data[REQUESTED_TOOLS_KEY]) {
@@ -592,7 +818,7 @@ async function getRequestedToolsOptions(variant) {
592
818
  }
593
819
  return Array.from(toolToCommands.entries()).map(([tool, commands]) => ({
594
820
  value: tool,
595
- label: extractLabelFromTool(tool),
821
+ label: tool,
596
822
  hint: formatCommandsHint(commands)
597
823
  }));
598
824
  }
@@ -601,13 +827,44 @@ function getDestinationPath(outputPath, scope) {
601
827
  return outputPath;
602
828
  }
603
829
  if (scope === SCOPES.PROJECT) {
604
- return path2.join(process.cwd(), DIRECTORIES.CLAUDE, DIRECTORIES.COMMANDS);
830
+ return path4.join(process.cwd(), DIRECTORIES.CLAUDE, DIRECTORIES.COMMANDS);
605
831
  }
606
832
  if (scope === SCOPES.USER) {
607
- return path2.join(os.homedir(), DIRECTORIES.CLAUDE, DIRECTORIES.COMMANDS);
833
+ return path4.join(os.homedir(), DIRECTORIES.CLAUDE, DIRECTORIES.COMMANDS);
608
834
  }
609
835
  throw new Error("Either outputPath or scope must be provided");
610
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
+ }
611
868
  function extractTemplateBlocks(content) {
612
869
  const blocks = [];
613
870
  const withCommandsRegex = /<claude-commands-template\s+commands="([^"]+)">([\s\S]*?)<\/claude-commands-template>/g;
@@ -625,34 +882,38 @@ function extractTemplateBlocks(content) {
625
882
  }
626
883
  return blocks;
627
884
  }
628
- async function generateToDirectory(outputPath, variant, scope, options) {
629
- const sourcePath = path2.join(
630
- __dirname2,
631
- "..",
632
- DIRECTORIES.DOWNLOADS,
633
- variant || VARIANTS.WITH_BEADS
634
- );
885
+ async function generateToDirectory(outputPath, scope, options) {
635
886
  const destinationPath = getDestinationPath(outputPath, scope);
636
- const allFiles = await fs.readdir(sourcePath);
887
+ const sourcePath = path4.join(__dirname3, "..", DIRECTORIES.SOURCES);
888
+ const flags = options?.flags ?? [];
889
+ const allFiles = (await fs4.readdir(sourcePath)).filter(
890
+ (f) => f.endsWith(".md")
891
+ );
637
892
  let files = options?.commands ? allFiles.filter((f) => options.commands.includes(f)) : allFiles;
638
893
  if (options?.skipFiles) {
639
894
  const prefix2 = options?.commandPrefix || "";
640
895
  files = files.filter((f) => !options.skipFiles.includes(prefix2 + f));
641
896
  }
642
897
  const prefix = options?.commandPrefix || "";
643
- if (options?.commands || options?.skipFiles || options?.commandPrefix) {
644
- await fs.ensureDir(destinationPath);
645
- for (const file of files) {
646
- await fs.copy(
647
- path2.join(sourcePath, file),
648
- path2.join(destinationPath, prefix + file)
649
- );
650
- }
651
- } else {
652
- await fs.copy(sourcePath, destinationPath, {});
898
+ await fs4.ensureDir(destinationPath);
899
+ const baseDir = path4.join(__dirname3, "..");
900
+ for (const file of files) {
901
+ const sourceFilePath = path4.join(sourcePath, file);
902
+ const sourceContent = await fs4.readFile(sourceFilePath, "utf-8");
903
+ const expandedContent = expandContent(sourceContent, {
904
+ flags,
905
+ baseDir
906
+ });
907
+ const cleanedContent = applyMarkdownFixes(
908
+ stripInternalMetadata(expandedContent)
909
+ );
910
+ await fs4.writeFile(
911
+ path4.join(destinationPath, prefix + file),
912
+ cleanedContent
913
+ );
653
914
  }
654
915
  if (options?.allowedTools && options.allowedTools.length > 0) {
655
- const metadata = await loadCommandsMetadata(variant || VARIANTS.WITH_BEADS);
916
+ const metadata = await loadCommandsMetadata();
656
917
  const allowedToolsSet = new Set(options.allowedTools);
657
918
  for (const file of files) {
658
919
  const commandMetadata = metadata[file];
@@ -661,8 +922,8 @@ async function generateToDirectory(outputPath, variant, scope, options) {
661
922
  (tool) => allowedToolsSet.has(tool)
662
923
  );
663
924
  if (toolsForCommand.length > 0) {
664
- const filePath = path2.join(destinationPath, prefix + file);
665
- const content = await fs.readFile(filePath, "utf-8");
925
+ const filePath = path4.join(destinationPath, prefix + file);
926
+ const content = await fs4.readFile(filePath, "utf-8");
666
927
  const allowedToolsYaml = `allowed-tools: ${toolsForCommand.join(", ")}`;
667
928
  const modifiedContent = content.replace(
668
929
  /^---\n/,
@@ -670,7 +931,7 @@ async function generateToDirectory(outputPath, variant, scope, options) {
670
931
  ${allowedToolsYaml}
671
932
  `
672
933
  );
673
- await fs.writeFile(filePath, modifiedContent);
934
+ await fs4.writeFile(filePath, modifiedContent);
674
935
  }
675
936
  }
676
937
  }
@@ -678,21 +939,21 @@ ${allowedToolsYaml}
678
939
  if (!options?.skipTemplateInjection) {
679
940
  let templateSourcePath = null;
680
941
  for (const filename of TEMPLATE_SOURCE_FILES) {
681
- const candidatePath = path2.join(process.cwd(), filename);
682
- if (await fs.pathExists(candidatePath)) {
942
+ const candidatePath = path4.join(process.cwd(), filename);
943
+ if (await fs4.pathExists(candidatePath)) {
683
944
  templateSourcePath = candidatePath;
684
945
  break;
685
946
  }
686
947
  }
687
948
  if (templateSourcePath) {
688
- const sourceContent = await fs.readFile(templateSourcePath, "utf-8");
949
+ const sourceContent = await fs4.readFile(templateSourcePath, "utf-8");
689
950
  const templates = extractTemplateBlocks(sourceContent);
690
951
  if (templates.length > 0) {
691
952
  for (const file of files) {
692
- const commandName = path2.basename(file, ".md");
953
+ const commandName = path4.basename(file, ".md");
693
954
  const actualFileName = options?.commandPrefix ? options.commandPrefix + file : file;
694
- const filePath = path2.join(destinationPath, actualFileName);
695
- let content = await fs.readFile(filePath, "utf-8");
955
+ const filePath = path4.join(destinationPath, actualFileName);
956
+ let content = await fs4.readFile(filePath, "utf-8");
696
957
  let modified = false;
697
958
  for (const template of templates) {
698
959
  if (template.commands && !template.commands.includes(commandName)) {
@@ -702,7 +963,7 @@ ${allowedToolsYaml}
702
963
  modified = true;
703
964
  }
704
965
  if (modified) {
705
- await fs.writeFile(filePath, content);
966
+ await fs4.writeFile(filePath, applyMarkdownFixes(content));
706
967
  }
707
968
  }
708
969
  templateInjected = true;
@@ -712,88 +973,12 @@ ${allowedToolsYaml}
712
973
  return {
713
974
  success: true,
714
975
  filesGenerated: files.length,
715
- variant,
716
976
  templateInjectionSkipped: options?.skipTemplateInjection,
717
- templateInjected
977
+ templateInjected,
978
+ flags: options?.flags
718
979
  };
719
980
  }
720
981
 
721
- // scripts/cli-options.ts
722
- init_esm_shims();
723
- var CLI_OPTIONS = [
724
- {
725
- flag: "--variant",
726
- key: "variant",
727
- type: "string",
728
- description: "Command variant (with-beads, without-beads)",
729
- example: "--variant=with-beads",
730
- requiredForNonInteractive: true
731
- },
732
- {
733
- flag: "--scope",
734
- key: "scope",
735
- type: "string",
736
- description: "Installation scope (project, user)",
737
- example: "--scope=project",
738
- requiredForNonInteractive: true
739
- },
740
- {
741
- flag: "--prefix",
742
- key: "prefix",
743
- type: "string",
744
- description: "Add prefix to command names",
745
- example: "--prefix=my-"
746
- },
747
- {
748
- flag: "--commands",
749
- key: "commands",
750
- type: "array",
751
- description: "Install only specific commands",
752
- example: "--commands=commit,red,green"
753
- },
754
- {
755
- flag: "--skip-template-injection",
756
- key: "skipTemplateInjection",
757
- type: "boolean",
758
- description: "Skip injecting project CLAUDE.md customizations"
759
- },
760
- {
761
- flag: "--update-existing",
762
- key: "updateExisting",
763
- type: "boolean",
764
- description: "Only update already-installed commands"
765
- },
766
- {
767
- flag: "--overwrite",
768
- key: "overwrite",
769
- type: "boolean",
770
- description: "Overwrite conflicting files without prompting"
771
- },
772
- {
773
- flag: "--skip-on-conflict",
774
- key: "skipOnConflict",
775
- type: "boolean",
776
- description: "Skip conflicting files without prompting"
777
- }
778
- ];
779
- function generateHelpText() {
780
- const lines = [
781
- "Usage: npx @wbern/claude-instructions [options]",
782
- "",
783
- "Options:"
784
- ];
785
- for (const opt of CLI_OPTIONS) {
786
- const suffix = opt.type === "string" ? "=<value>" : opt.type === "array" ? "=<list>" : "";
787
- const padding = 28 - (opt.flag.length + suffix.length);
788
- lines.push(
789
- ` ${opt.flag}${suffix}${" ".repeat(Math.max(1, padding))}${opt.description}`
790
- );
791
- }
792
- lines.push(" --help, -h Show this help message");
793
- lines.push(" --version, -v Show version number");
794
- return lines.join("\n");
795
- }
796
-
797
982
  // scripts/tty.ts
798
983
  init_esm_shims();
799
984
  function isInteractiveTTY() {
@@ -802,6 +987,10 @@ function isInteractiveTTY() {
802
987
 
803
988
  // scripts/cli.ts
804
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));
805
994
  function splitChangeIntoLines(value) {
806
995
  const lines = value.split("\n");
807
996
  if (lines[lines.length - 1] === "") lines.pop();
@@ -919,24 +1108,22 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
919
1108
  `;
920
1109
  async function main(args) {
921
1110
  intro(BATMAN_LOGO);
922
- let variant;
923
1111
  let scope;
924
1112
  let commandPrefix;
925
1113
  let selectedCommands;
926
1114
  let selectedAllowedTools;
1115
+ let selectedFlags;
927
1116
  let cachedExistingFiles;
928
- if (args?.variant && args?.scope) {
929
- variant = args.variant;
930
- scope = args.scope;
1117
+ if (args?.scope) {
1118
+ scope = v2.parse(ScopeSchema, args.scope);
931
1119
  commandPrefix = args.prefix ?? "";
932
1120
  selectedCommands = args.commands;
1121
+ selectedFlags = args.flags ? v2.parse(FlagsSchema, args.flags) : void 0;
933
1122
  if (args.updateExisting) {
934
- cachedExistingFiles = await checkExistingFiles(
935
- void 0,
936
- variant,
937
- scope,
938
- { commandPrefix: commandPrefix || "" }
939
- );
1123
+ cachedExistingFiles = await checkExistingFiles(void 0, scope, {
1124
+ commandPrefix: commandPrefix || "",
1125
+ flags: selectedFlags
1126
+ });
940
1127
  selectedCommands = cachedExistingFiles.map((f) => f.filename);
941
1128
  if (selectedCommands.length === 0) {
942
1129
  log.warn("No existing commands found in target directory");
@@ -951,13 +1138,6 @@ async function main(args) {
951
1138
  log.warn(`Non-interactive mode requires ${requiredFlags} arguments`);
952
1139
  return;
953
1140
  }
954
- variant = await select({
955
- message: "Select variant",
956
- options: [...VARIANT_OPTIONS]
957
- });
958
- if (isCancel(variant)) {
959
- return;
960
- }
961
1141
  const terminalWidth = process.stdout.columns || 80;
962
1142
  const uiOverhead = 25;
963
1143
  scope = await select({
@@ -974,15 +1154,29 @@ async function main(args) {
974
1154
  if (isCancel(commandPrefix)) {
975
1155
  return;
976
1156
  }
977
- let groupedCommands = await getCommandsGroupedByCategory(
978
- variant
979
- );
1157
+ selectedFlags = await groupMultiselect({
1158
+ message: "Select feature flags (optional)",
1159
+ options: {
1160
+ "Feature Flags": FLAG_OPTIONS.map(({ value, label, hint }) => ({
1161
+ value,
1162
+ label,
1163
+ hint
1164
+ }))
1165
+ },
1166
+ required: false
1167
+ });
1168
+ if (isCancel(selectedFlags)) {
1169
+ return;
1170
+ }
1171
+ let groupedCommands = await getCommandsGroupedByCategory();
980
1172
  if (args?.updateExisting) {
981
1173
  cachedExistingFiles = await checkExistingFiles(
982
1174
  void 0,
983
- variant,
984
1175
  scope,
985
- { commandPrefix: commandPrefix || "" }
1176
+ {
1177
+ commandPrefix: commandPrefix || "",
1178
+ flags: selectedFlags
1179
+ }
986
1180
  );
987
1181
  const existingFilenames = new Set(
988
1182
  cachedExistingFiles.map((f) => f.filename)
@@ -1011,9 +1205,7 @@ async function main(args) {
1011
1205
  if (isCancel(selectedCommands)) {
1012
1206
  return;
1013
1207
  }
1014
- const requestedToolsOptions = await getRequestedToolsOptions(
1015
- variant
1016
- );
1208
+ const requestedToolsOptions = await getRequestedToolsOptions();
1017
1209
  if (requestedToolsOptions.length > 0) {
1018
1210
  selectedAllowedTools = await groupMultiselect({
1019
1211
  message: "Select allowed tools for commands (optional)",
@@ -1027,21 +1219,20 @@ async function main(args) {
1027
1219
  }
1028
1220
  }
1029
1221
  }
1030
- const existingFiles = cachedExistingFiles ?? await checkExistingFiles(void 0, variant, scope, {
1222
+ const existingFiles = cachedExistingFiles ?? await checkExistingFiles(void 0, scope, {
1031
1223
  commandPrefix,
1032
1224
  commands: selectedCommands,
1033
- allowedTools: selectedAllowedTools
1225
+ allowedTools: selectedAllowedTools,
1226
+ flags: selectedFlags
1034
1227
  });
1035
1228
  const skipFiles = [];
1229
+ const conflictingFiles = existingFiles.filter((f) => !f.isIdentical);
1036
1230
  const shouldSkipConflicts = args?.skipOnConflict || !isInteractiveTTY();
1037
1231
  if (args?.overwrite) {
1038
- for (const file of existingFiles) {
1039
- if (!file.isIdentical) {
1040
- log.info(`Overwriting ${file.filename}`);
1041
- }
1232
+ for (const file of conflictingFiles) {
1233
+ log.info(`Overwriting ${file.filename}`);
1042
1234
  }
1043
1235
  } else if (!shouldSkipConflicts) {
1044
- const conflictingFiles = existingFiles.filter((f) => !f.isIdentical);
1045
1236
  const hasMultipleConflicts = conflictingFiles.length > 1;
1046
1237
  let overwriteAllSelected = false;
1047
1238
  let skipAllSelected = false;
@@ -1095,31 +1286,25 @@ async function main(args) {
1095
1286
  }
1096
1287
  }
1097
1288
  }
1098
- } else if (shouldSkipConflicts) {
1099
- for (const file of existingFiles) {
1100
- if (!file.isIdentical) {
1101
- skipFiles.push(file.filename);
1102
- log.warn(`Skipping ${file.filename} (conflict)`);
1103
- }
1289
+ } else {
1290
+ for (const file of conflictingFiles) {
1291
+ skipFiles.push(file.filename);
1292
+ log.warn(`Skipping ${file.filename} (conflict)`);
1104
1293
  }
1105
- if (skipFiles.length > 0 && !isInteractiveTTY()) {
1294
+ if (conflictingFiles.length > 0 && !isInteractiveTTY()) {
1106
1295
  log.info(
1107
1296
  "To resolve conflicts, run interactively or use --overwrite to overwrite"
1108
1297
  );
1109
1298
  }
1110
1299
  }
1111
- const result = await generateToDirectory(
1112
- void 0,
1113
- variant,
1114
- scope,
1115
- {
1116
- commandPrefix,
1117
- skipTemplateInjection: args?.skipTemplateInjection,
1118
- commands: selectedCommands,
1119
- skipFiles,
1120
- allowedTools: selectedAllowedTools
1121
- }
1122
- );
1300
+ const result = await generateToDirectory(void 0, scope, {
1301
+ commandPrefix,
1302
+ skipTemplateInjection: args?.skipTemplateInjection,
1303
+ commands: selectedCommands,
1304
+ skipFiles,
1305
+ allowedTools: selectedAllowedTools,
1306
+ flags: selectedFlags
1307
+ });
1123
1308
  const fullPath = scope === "project" ? `${process.cwd()}/.claude/commands` : `${os2.homedir()}/.claude/commands`;
1124
1309
  outro(
1125
1310
  `Installed ${result.filesGenerated} commands to ${fullPath}