@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 +2 -2
- package/bin/cli.js +161 -88
- package/package.json +7 -4
- package/src/README.md +2 -2
- package/src/fragments/no-plan-files.md +3 -0
- package/src/sources/_contribute-a-command.md +137 -0
- package/src/sources/commit.md +3 -0
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
|
-
- **
|
|
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 --
|
|
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
|
|
51
|
-
return ~index ? open + replaceClose(
|
|
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 = (
|
|
53
|
+
var replaceClose = (string2, close, replace, index) => {
|
|
54
54
|
let result = "", cursor = 0;
|
|
55
55
|
do {
|
|
56
|
-
result +=
|
|
56
|
+
result += string2.substring(cursor, index) + replace;
|
|
57
57
|
cursor = index + close.length;
|
|
58
|
-
index =
|
|
58
|
+
index = string2.indexOf(close, cursor);
|
|
59
59
|
} while (~index);
|
|
60
|
-
return result +
|
|
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(
|
|
275
|
+
removeEmpty(array2) {
|
|
276
276
|
const ret = [];
|
|
277
|
-
for (let i = 0; i <
|
|
278
|
-
if (
|
|
279
|
-
ret.push(
|
|
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
|
|
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
|
-
|
|
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
|
|
439
|
+
return fs2.readFileSync(fullElsePath, "utf8");
|
|
427
440
|
} catch (err) {
|
|
428
441
|
throw new Error(
|
|
429
|
-
`Failed to read elsePath '${elsePath}': ${
|
|
442
|
+
`Failed to read elsePath '${elsePath}': ${getErrorMessage(err)}`
|
|
430
443
|
);
|
|
431
444
|
}
|
|
432
445
|
}
|
|
433
446
|
return "";
|
|
434
447
|
}
|
|
435
448
|
if (!includePath) {
|
|
436
|
-
|
|
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
|
|
453
|
+
return fs2.readFileSync(fullPath, "utf8");
|
|
441
454
|
} catch (err) {
|
|
442
455
|
throw new Error(
|
|
443
|
-
`Failed to read '${includePath}': ${
|
|
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
|
-
|
|
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:
|
|
630
|
+
description: validated.description,
|
|
606
631
|
hint: frontmatter._hint,
|
|
607
632
|
category: getCategory(frontmatter),
|
|
608
|
-
order:
|
|
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 =
|
|
721
|
+
const destinationPath = getDestinationPath(outputPath, scope);
|
|
688
722
|
const flags = options?.flags ?? [];
|
|
689
|
-
const allFiles = await
|
|
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
|
|
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 =
|
|
708
|
-
|
|
709
|
-
|
|
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:
|
|
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 =
|
|
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 +
|
|
868
|
-
|
|
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
|
|
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
|
|
910
|
-
const
|
|
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
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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:
|
|
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
|
|
1180
|
-
|
|
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
|
|
1240
|
-
for (const file of
|
|
1241
|
-
|
|
1242
|
-
|
|
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 (
|
|
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.
|
|
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": "
|
|
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
|
-
- **
|
|
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 --
|
|
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,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
|
package/src/sources/commit.md
CHANGED
|
@@ -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
|