@hacksmith/doraval 0.2.21 → 0.2.25
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 +8 -5
- package/bin/doraval.js +1487 -250
- package/package.json +1 -1
package/bin/doraval.js
CHANGED
|
@@ -595,6 +595,71 @@ var init_dist = __esm(() => {
|
|
|
595
595
|
negativePrefixRe = /^no[-A-Z]/;
|
|
596
596
|
});
|
|
597
597
|
|
|
598
|
+
// package.json
|
|
599
|
+
var require_package = __commonJS((exports, module) => {
|
|
600
|
+
module.exports = {
|
|
601
|
+
name: "@hacksmith/doraval",
|
|
602
|
+
version: "0.2.25",
|
|
603
|
+
author: "Saif",
|
|
604
|
+
repository: {
|
|
605
|
+
type: "git",
|
|
606
|
+
url: "git+https://github.com/saif-shines/doraval.git"
|
|
607
|
+
},
|
|
608
|
+
devDependencies: {
|
|
609
|
+
"@types/bun": "latest"
|
|
610
|
+
},
|
|
611
|
+
bin: {
|
|
612
|
+
doraval: "bin/doraval-wrapper.js",
|
|
613
|
+
dora: "bin/doraval-wrapper.js"
|
|
614
|
+
},
|
|
615
|
+
description: "The context engineering toolkit for coding agents",
|
|
616
|
+
engines: {
|
|
617
|
+
bun: ">=1.2.0",
|
|
618
|
+
node: ">=14.18.0"
|
|
619
|
+
},
|
|
620
|
+
files: [
|
|
621
|
+
"bin/",
|
|
622
|
+
"dist/",
|
|
623
|
+
"README.md"
|
|
624
|
+
],
|
|
625
|
+
keywords: [
|
|
626
|
+
"cli",
|
|
627
|
+
"skills",
|
|
628
|
+
"plugins",
|
|
629
|
+
"agent",
|
|
630
|
+
"validation",
|
|
631
|
+
"lint",
|
|
632
|
+
"claude-code",
|
|
633
|
+
"grok",
|
|
634
|
+
"cursor",
|
|
635
|
+
"windsurf",
|
|
636
|
+
"mcp"
|
|
637
|
+
],
|
|
638
|
+
license: "MIT",
|
|
639
|
+
workspaces: [
|
|
640
|
+
"apps/*"
|
|
641
|
+
],
|
|
642
|
+
scripts: {
|
|
643
|
+
build: "bun build ./src/cli/index.ts --outfile ./bin/doraval.js --target bun",
|
|
644
|
+
dev: "bun run ./src/cli/index.ts",
|
|
645
|
+
test: "bun test",
|
|
646
|
+
typecheck: "bunx tsc --noEmit --skipLibCheck",
|
|
647
|
+
prepublishOnly: `bun run build && node -e "const p=require('./package.json'),j=require('./jsr.json');if(p.version!==j.version){console.error('Version mismatch: package.json='+p.version+' jsr.json='+j.version);process.exit(1)}"`,
|
|
648
|
+
bump: "bun run scripts/bump.ts",
|
|
649
|
+
release: "bun run scripts/release.ts",
|
|
650
|
+
"jsr:publish": "bunx jsr publish",
|
|
651
|
+
"site:dev": "cd apps/website && bun run dev",
|
|
652
|
+
"site:build": "cd apps/website && bun run build",
|
|
653
|
+
"site:preview": "cd apps/website && bun run preview"
|
|
654
|
+
},
|
|
655
|
+
type: "module",
|
|
656
|
+
dependencies: {
|
|
657
|
+
citty: "^0.2.2",
|
|
658
|
+
picocolors: "^1.1.1"
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
});
|
|
662
|
+
|
|
598
663
|
// node_modules/.bun/picocolors@1.1.1/node_modules/picocolors/picocolors.js
|
|
599
664
|
var require_picocolors = __commonJS((exports, module) => {
|
|
600
665
|
var p = process || {};
|
|
@@ -2624,17 +2689,17 @@ __export(exports_new, {
|
|
|
2624
2689
|
default: () => new_default,
|
|
2625
2690
|
decidePath: () => decidePath
|
|
2626
2691
|
});
|
|
2627
|
-
import { join as join8 } from "path";
|
|
2692
|
+
import { join as join8, basename as basename2 } from "path";
|
|
2628
2693
|
import { mkdirSync as mkdirSync2, writeFileSync, existsSync as existsSync9 } from "fs";
|
|
2629
2694
|
function decidePath(ctx, intent, providedName) {
|
|
2630
2695
|
const rawName = providedName || "";
|
|
2631
|
-
let
|
|
2696
|
+
let decisionPath = "standalone";
|
|
2632
2697
|
let targetDir = ctx.cwd;
|
|
2633
2698
|
let shouldCreateDir = false;
|
|
2634
2699
|
let migrateExisting = false;
|
|
2635
|
-
const useCurrentDirAsRoot = rawName === "." || rawName ===
|
|
2700
|
+
const useCurrentDirAsRoot = rawName === "." || rawName === basename2(ctx.cwd) || !rawName;
|
|
2636
2701
|
if (intent === "distribute" || intent === "self-later" && ctx.looseSkillFiles.length > 0 && !ctx.hasClaudeDir) {
|
|
2637
|
-
|
|
2702
|
+
decisionPath = "plugin";
|
|
2638
2703
|
if (useCurrentDirAsRoot) {
|
|
2639
2704
|
targetDir = ctx.cwd;
|
|
2640
2705
|
shouldCreateDir = false;
|
|
@@ -2644,7 +2709,7 @@ function decidePath(ctx, intent, providedName) {
|
|
|
2644
2709
|
}
|
|
2645
2710
|
migrateExisting = ctx.looseSkillFiles.length > 0;
|
|
2646
2711
|
} else if (intent === "self-later" && !ctx.hasClaudeDir) {
|
|
2647
|
-
|
|
2712
|
+
decisionPath = "plugin";
|
|
2648
2713
|
if (useCurrentDirAsRoot) {
|
|
2649
2714
|
targetDir = ctx.cwd;
|
|
2650
2715
|
shouldCreateDir = false;
|
|
@@ -2652,7 +2717,7 @@ function decidePath(ctx, intent, providedName) {
|
|
|
2652
2717
|
targetDir = join8(ctx.cwd, rawName);
|
|
2653
2718
|
shouldCreateDir = true;
|
|
2654
2719
|
}
|
|
2655
|
-
} else if (
|
|
2720
|
+
} else if (decisionPath === "standalone") {
|
|
2656
2721
|
if (useCurrentDirAsRoot) {
|
|
2657
2722
|
targetDir = ctx.cwd;
|
|
2658
2723
|
shouldCreateDir = false;
|
|
@@ -2661,7 +2726,7 @@ function decidePath(ctx, intent, providedName) {
|
|
|
2661
2726
|
shouldCreateDir = true;
|
|
2662
2727
|
}
|
|
2663
2728
|
}
|
|
2664
|
-
return { path, targetDir, shouldCreateDir, migrateExisting };
|
|
2729
|
+
return { path: decisionPath, targetDir, shouldCreateDir, migrateExisting };
|
|
2665
2730
|
}
|
|
2666
2731
|
function scaffold(decision, ctx, migrateContent) {
|
|
2667
2732
|
const { targetDir, path, shouldCreateDir } = decision;
|
|
@@ -2673,26 +2738,57 @@ function scaffold(decision, ctx, migrateContent) {
|
|
|
2673
2738
|
mkdirSync2(targetDir, { recursive: true });
|
|
2674
2739
|
}
|
|
2675
2740
|
if (path === "plugin") {
|
|
2741
|
+
const pluginName = basename2(targetDir);
|
|
2676
2742
|
const pluginJson = {
|
|
2677
|
-
name:
|
|
2743
|
+
name: pluginName,
|
|
2678
2744
|
description: "Scaffolded by doraval claude new",
|
|
2679
2745
|
version: "0.1.0"
|
|
2680
2746
|
};
|
|
2681
2747
|
mkdirSync2(join8(targetDir, ".claude-plugin"), { recursive: true });
|
|
2682
2748
|
writeFileSync(join8(targetDir, ".claude-plugin", "plugin.json"), JSON.stringify(pluginJson, null, 2));
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2749
|
+
const marketplaceJson = {
|
|
2750
|
+
name: pluginName,
|
|
2751
|
+
version: "0.1.0",
|
|
2752
|
+
description: "Scaffolded by doraval claude new",
|
|
2753
|
+
author: { name: "" },
|
|
2754
|
+
homepage: "",
|
|
2755
|
+
repository: "",
|
|
2756
|
+
license: "MIT",
|
|
2757
|
+
keywords: ["claude-code", "skills", "plugin"]
|
|
2758
|
+
};
|
|
2759
|
+
writeFileSync(join8(targetDir, "marketplace.json"), JSON.stringify(marketplaceJson, null, 2));
|
|
2760
|
+
const demoSkillName = "doraval";
|
|
2761
|
+
mkdirSync2(join8(targetDir, "skills", demoSkillName), { recursive: true });
|
|
2762
|
+
let skillContent;
|
|
2763
|
+
if (migrateContent) {
|
|
2764
|
+
skillContent = migrateContent;
|
|
2765
|
+
} else {
|
|
2766
|
+
skillContent = `---
|
|
2767
|
+
name: ${demoSkillName}
|
|
2768
|
+
description: Use doraval to validate, measure drift, and judge skills and plugins. Use when authoring or reviewing context engineering artifacts for AI coding agents.
|
|
2690
2769
|
---
|
|
2691
2770
|
|
|
2692
|
-
|
|
2693
|
-
|
|
2771
|
+
# Use Doraval
|
|
2772
|
+
|
|
2773
|
+
Doraval is the context engineering toolkit.
|
|
2774
|
+
|
|
2775
|
+
When you need to check a skill or plugin:
|
|
2776
|
+
|
|
2777
|
+
- Validate the current directory: \`doraval validate .\`
|
|
2778
|
+
- Validate a specific plugin: \`doraval validate . --for claude:plugin\`
|
|
2779
|
+
- Validate one skill: \`doraval skill validate ./skills/${demoSkillName}/\`
|
|
2780
|
+
- Check for rubric drift: \`doraval skill drift ./skills/${demoSkillName}/\`
|
|
2781
|
+
- Get an AI quality judgment: \`doraval skill judge ./skills/${demoSkillName}/\`
|
|
2782
|
+
|
|
2783
|
+
Always run \`doraval validate\` before sharing or publishing a plugin. This skill demonstrates a complete, self-referential example of using doraval inside a generated plugin.`;
|
|
2784
|
+
}
|
|
2785
|
+
writeFileSync(join8(targetDir, "skills", demoSkillName, "SKILL.md"), skillContent);
|
|
2786
|
+
const readmePath = join8(targetDir, "README.md");
|
|
2787
|
+
if (!existsSync9(readmePath)) {
|
|
2788
|
+
writeFileSync(readmePath, "# " + pluginName + `
|
|
2694
2789
|
|
|
2695
2790
|
Claude Code plugin scaffolded by doraval.`);
|
|
2791
|
+
}
|
|
2696
2792
|
} else {
|
|
2697
2793
|
mkdirSync2(join8(targetDir, ".claude", "skills", "my-skill"), { recursive: true });
|
|
2698
2794
|
const skillBody = migrateContent || `# My Skill
|
|
@@ -2752,7 +2848,12 @@ var init_new = __esm(() => {
|
|
|
2752
2848
|
scaffold(decision, ctx, migrateContent);
|
|
2753
2849
|
ui.write(`
|
|
2754
2850
|
${import_picocolors10.default.green("\u2713")} Created ${decision.path} at ${import_picocolors10.default.bold(decision.targetDir)}`);
|
|
2755
|
-
|
|
2851
|
+
const cmdName = decision.path === "plugin" ? `/${basename2(decision.targetDir)}:doraval` : "/my-skill";
|
|
2852
|
+
ui.info(` Command: ${cmdName}`);
|
|
2853
|
+
if (decision.path === "plugin") {
|
|
2854
|
+
ui.info(` Claude: .claude-plugin/plugin.json`);
|
|
2855
|
+
ui.info(` Marketplace: marketplace.json (unified / cross-provider listings)`);
|
|
2856
|
+
}
|
|
2756
2857
|
ui.info(` Test: claude --plugin-dir ${decision.targetDir} (or use normally for standalone)`);
|
|
2757
2858
|
ui.info(` Validate: doraval validate ${decision.targetDir}`);
|
|
2758
2859
|
if (decision.path === "plugin" && decision.migrateExisting) {
|
|
@@ -2763,9 +2864,472 @@ var init_new = __esm(() => {
|
|
|
2763
2864
|
});
|
|
2764
2865
|
});
|
|
2765
2866
|
|
|
2867
|
+
// src/cli/commands/bump.ts
|
|
2868
|
+
var exports_bump = {};
|
|
2869
|
+
__export(exports_bump, {
|
|
2870
|
+
default: () => bump_default
|
|
2871
|
+
});
|
|
2872
|
+
import { resolve as resolve3, join as join9, dirname, relative } from "path";
|
|
2873
|
+
import { existsSync as existsSync10, readFileSync, writeFileSync as writeFileSync2, readdirSync as readdirSync4, statSync } from "fs";
|
|
2874
|
+
function bumpVersion(current, type) {
|
|
2875
|
+
if (/^\d+\.\d+\.\d+$/.test(type))
|
|
2876
|
+
return type;
|
|
2877
|
+
const curr = current || "0.0.0";
|
|
2878
|
+
const parts = curr.split(".").map((n) => parseInt(n, 10) || 0);
|
|
2879
|
+
const [major = 0, minor = 0, patch = 0] = parts;
|
|
2880
|
+
switch (type) {
|
|
2881
|
+
case "patch":
|
|
2882
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
2883
|
+
case "minor":
|
|
2884
|
+
return `${major}.${minor + 1}.0`;
|
|
2885
|
+
case "major":
|
|
2886
|
+
return `${major + 1}.0.0`;
|
|
2887
|
+
default:
|
|
2888
|
+
throw new Error(`Invalid bump type "${type}". Use patch, minor, major, or an exact version like 1.2.3`);
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
function readJson(p) {
|
|
2892
|
+
try {
|
|
2893
|
+
const content = readFileSync(p, "utf8");
|
|
2894
|
+
return JSON.parse(content);
|
|
2895
|
+
} catch {
|
|
2896
|
+
return null;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
function writeJson(p, data) {
|
|
2900
|
+
writeFileSync2(p, JSON.stringify(data, null, 2) + `
|
|
2901
|
+
`, "utf8");
|
|
2902
|
+
}
|
|
2903
|
+
function getVersion(obj) {
|
|
2904
|
+
if (!obj || typeof obj !== "object")
|
|
2905
|
+
return;
|
|
2906
|
+
if (typeof obj.version === "string")
|
|
2907
|
+
return obj.version;
|
|
2908
|
+
if (obj.metadata && typeof obj.metadata.version === "string")
|
|
2909
|
+
return obj.metadata.version;
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
function setVersion(obj, newVersion) {
|
|
2913
|
+
if (!obj || typeof obj !== "object")
|
|
2914
|
+
return false;
|
|
2915
|
+
if (typeof obj.version === "string") {
|
|
2916
|
+
obj.version = newVersion;
|
|
2917
|
+
return true;
|
|
2918
|
+
}
|
|
2919
|
+
if (obj.metadata && typeof obj.metadata.version === "string") {
|
|
2920
|
+
obj.metadata.version = newVersion;
|
|
2921
|
+
return true;
|
|
2922
|
+
}
|
|
2923
|
+
return false;
|
|
2924
|
+
}
|
|
2925
|
+
function walkForTargets(dir, maxDepth = 6, currentDepth = 0) {
|
|
2926
|
+
const results = [];
|
|
2927
|
+
if (currentDepth > maxDepth)
|
|
2928
|
+
return results;
|
|
2929
|
+
let entries;
|
|
2930
|
+
try {
|
|
2931
|
+
entries = readdirSync4(dir);
|
|
2932
|
+
} catch {
|
|
2933
|
+
return results;
|
|
2934
|
+
}
|
|
2935
|
+
for (const entry of entries) {
|
|
2936
|
+
const full = join9(dir, entry);
|
|
2937
|
+
let st;
|
|
2938
|
+
try {
|
|
2939
|
+
st = statSync(full);
|
|
2940
|
+
} catch {
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
if (st.isDirectory()) {
|
|
2944
|
+
const sub = walkForTargets(full, maxDepth, currentDepth + 1);
|
|
2945
|
+
results.push(...sub);
|
|
2946
|
+
} else if (st.isFile()) {
|
|
2947
|
+
if (entry === "plugin.json") {
|
|
2948
|
+
const parentDir = dirname(full);
|
|
2949
|
+
const parentName = parentDir.split(/[/\\]/).pop();
|
|
2950
|
+
if (parentName === ".claude-plugin" || parentName === ".codex-plugin" || parentName === ".cursor-plugin") {
|
|
2951
|
+
results.push({
|
|
2952
|
+
file: full,
|
|
2953
|
+
kind: "plugin",
|
|
2954
|
+
label: `plugin manifest (${parentName.replace(".", "")})`
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
} else if (entry === "marketplace.json") {
|
|
2958
|
+
const json = readJson(full);
|
|
2959
|
+
if (json && getVersion(json)) {
|
|
2960
|
+
results.push({
|
|
2961
|
+
file: full,
|
|
2962
|
+
kind: "marketplace",
|
|
2963
|
+
label: "marketplace.json"
|
|
2964
|
+
});
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
return results;
|
|
2970
|
+
}
|
|
2971
|
+
var import_picocolors11, bump_default;
|
|
2972
|
+
var init_bump = __esm(() => {
|
|
2973
|
+
init_dist();
|
|
2974
|
+
init_out();
|
|
2975
|
+
import_picocolors11 = __toESM(require_picocolors(), 1);
|
|
2976
|
+
bump_default = defineCommand({
|
|
2977
|
+
meta: {
|
|
2978
|
+
name: "bump",
|
|
2979
|
+
description: "Bump semver versions in plugin.json (manifests) and marketplace.json files (supports Claude, Codex, Cursor)"
|
|
2980
|
+
},
|
|
2981
|
+
args: {
|
|
2982
|
+
type: {
|
|
2983
|
+
type: "positional",
|
|
2984
|
+
description: "patch | minor | major | x.y.z (exact version)",
|
|
2985
|
+
required: false
|
|
2986
|
+
},
|
|
2987
|
+
path: {
|
|
2988
|
+
type: "positional",
|
|
2989
|
+
description: "Directory to scan from (defaults to current dir). Supports single plugin or marketplace root with many plugins/",
|
|
2990
|
+
required: false
|
|
2991
|
+
},
|
|
2992
|
+
only: {
|
|
2993
|
+
type: "string",
|
|
2994
|
+
description: 'Scope to "all" (default), "plugin" (only plugin.json manifests), or "marketplace" (only marketplace.json files that carry a top-level version)',
|
|
2995
|
+
default: "all"
|
|
2996
|
+
}
|
|
2997
|
+
},
|
|
2998
|
+
run({ args }) {
|
|
2999
|
+
let rawType = args.type || "patch";
|
|
3000
|
+
let targetPath = args.path || ".";
|
|
3001
|
+
const scopeInput = (args.only || "all").toLowerCase();
|
|
3002
|
+
const scope = scopeInput === "plugin" || scopeInput === "marketplace" ? scopeInput : "all";
|
|
3003
|
+
if (!["all", "plugin", "marketplace"].includes(scopeInput)) {
|
|
3004
|
+
ui.fail(`Invalid --only "${args.only}". Allowed: all, plugin, marketplace.`);
|
|
3005
|
+
process.exit(1);
|
|
3006
|
+
}
|
|
3007
|
+
const isKnownType = ["patch", "minor", "major"].includes(rawType) || /^\d+\.\d+\.\d+$/.test(rawType);
|
|
3008
|
+
const maybePath = resolve3(rawType);
|
|
3009
|
+
const looksLikeDir = existsSync10(maybePath) || rawType === "." || rawType.startsWith("./") || rawType.startsWith("../");
|
|
3010
|
+
if (!isKnownType && looksLikeDir) {
|
|
3011
|
+
targetPath = rawType;
|
|
3012
|
+
rawType = "patch";
|
|
3013
|
+
} else if (!isKnownType) {
|
|
3014
|
+
ui.fail(`Unknown bump type "${rawType}". Use patch | minor | major | 1.2.3`);
|
|
3015
|
+
process.exit(1);
|
|
3016
|
+
}
|
|
3017
|
+
const root = resolve3(targetPath);
|
|
3018
|
+
if (!existsSync10(root)) {
|
|
3019
|
+
ui.fail(`Path does not exist: ${root}`);
|
|
3020
|
+
process.exit(1);
|
|
3021
|
+
}
|
|
3022
|
+
ui.heading("doraval bump");
|
|
3023
|
+
ui.info(` scanning: ${root}`);
|
|
3024
|
+
ui.info(` scope: ${scope} (use --only plugin or --only marketplace to narrow; Cursor metadata.version supported)`);
|
|
3025
|
+
const discovered = walkForTargets(root);
|
|
3026
|
+
let targets = discovered;
|
|
3027
|
+
if (scope === "plugin") {
|
|
3028
|
+
targets = discovered.filter((t) => t.kind === "plugin");
|
|
3029
|
+
} else if (scope === "marketplace") {
|
|
3030
|
+
targets = discovered.filter((t) => t.kind === "marketplace");
|
|
3031
|
+
}
|
|
3032
|
+
if (targets.length === 0) {
|
|
3033
|
+
ui.fail("No matching files found under the scope.");
|
|
3034
|
+
ui.info("");
|
|
3035
|
+
ui.info(" Looked for (recursively):");
|
|
3036
|
+
ui.info(" \u2022 **/.claude-plugin/plugin.json");
|
|
3037
|
+
ui.info(" \u2022 **/.codex-plugin/plugin.json");
|
|
3038
|
+
ui.info(" \u2022 **/.cursor-plugin/plugin.json (or marketplace.json)");
|
|
3039
|
+
ui.info(" \u2022 **/marketplace.json (top-level version or metadata.version for Cursor)");
|
|
3040
|
+
ui.info("");
|
|
3041
|
+
ui.info(" Tip: run from inside a plugin directory, or pass a path that contains plugins/.");
|
|
3042
|
+
ui.info(" Examples:");
|
|
3043
|
+
ui.info(" dora bump minor");
|
|
3044
|
+
ui.info(" dora bump minor ./my-claude-plugin");
|
|
3045
|
+
ui.info(" dora bump --only plugin . # only the manifests");
|
|
3046
|
+
ui.info(" dora bump --only marketplace ./marketplaces-root # includes Cursor metadata.version");
|
|
3047
|
+
process.exit(1);
|
|
3048
|
+
}
|
|
3049
|
+
ui.info(` matched ${targets.length} file(s)`);
|
|
3050
|
+
let bumpedCount = 0;
|
|
3051
|
+
for (const t of targets) {
|
|
3052
|
+
const json = readJson(t.file);
|
|
3053
|
+
if (!json || typeof json !== "object") {
|
|
3054
|
+
ui.warnItem(`skipped (invalid JSON): ${relative(root, t.file)}`);
|
|
3055
|
+
continue;
|
|
3056
|
+
}
|
|
3057
|
+
const current = getVersion(json);
|
|
3058
|
+
let next;
|
|
3059
|
+
try {
|
|
3060
|
+
next = bumpVersion(current, rawType);
|
|
3061
|
+
} catch (err) {
|
|
3062
|
+
ui.fail(err.message || String(err));
|
|
3063
|
+
process.exit(1);
|
|
3064
|
+
}
|
|
3065
|
+
const relPath = relative(root, t.file);
|
|
3066
|
+
if (current === next) {
|
|
3067
|
+
ui.dim(` \u2022 ${t.label} ${current || "(no version)"} (no change) [${relPath}]`);
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
3070
|
+
const didUpdate = setVersion(json, next);
|
|
3071
|
+
if (!didUpdate) {
|
|
3072
|
+
ui.warnItem(`skipped (could not locate version field to update): ${relPath}`);
|
|
3073
|
+
continue;
|
|
3074
|
+
}
|
|
3075
|
+
writeJson(t.file, json);
|
|
3076
|
+
ui.success(`${t.label}: ${import_picocolors11.default.dim(current || "(none)")} \u2192 ${import_picocolors11.default.green(next)}`);
|
|
3077
|
+
ui.info(` ${relPath}`);
|
|
3078
|
+
bumpedCount++;
|
|
3079
|
+
}
|
|
3080
|
+
ui.blank();
|
|
3081
|
+
if (bumpedCount === 0) {
|
|
3082
|
+
ui.info("All matched files were already at the target version.");
|
|
3083
|
+
} else {
|
|
3084
|
+
ui.info(`Done. Bumped ${bumpedCount} file(s).`);
|
|
3085
|
+
ui.dim(" Next: doraval validate " + (targetPath === "." ? "." : targetPath));
|
|
3086
|
+
}
|
|
3087
|
+
process.exit(0);
|
|
3088
|
+
}
|
|
3089
|
+
});
|
|
3090
|
+
});
|
|
3091
|
+
|
|
3092
|
+
// src/cli/commands/codex/context.ts
|
|
3093
|
+
import { existsSync as existsSync11, readdirSync as readdirSync5 } from "fs";
|
|
3094
|
+
import { join as join10 } from "path";
|
|
3095
|
+
function detectContext2(cwd = process.cwd()) {
|
|
3096
|
+
const hasCodexDir = existsSync11(join10(cwd, ".codex"));
|
|
3097
|
+
const hasPluginManifest = existsSync11(join10(cwd, ".codex-plugin", "plugin.json"));
|
|
3098
|
+
const hasMarketplace = existsSync11(join10(cwd, ".agents", "plugins", "marketplace.json")) || existsSync11(join10(cwd, ".codex-plugin", "marketplace.json"));
|
|
3099
|
+
let looseSkillFiles = [];
|
|
3100
|
+
try {
|
|
3101
|
+
const files = readdirSync5(cwd);
|
|
3102
|
+
looseSkillFiles = files.filter((f) => {
|
|
3103
|
+
if (!f.endsWith(".md") || f.startsWith("."))
|
|
3104
|
+
return false;
|
|
3105
|
+
const lower = f.toLowerCase();
|
|
3106
|
+
if (lower === "readme.md" || lower === "changelog.md" || lower === "license.md" || lower.includes("contributing"))
|
|
3107
|
+
return false;
|
|
3108
|
+
return lower.includes("skill") || lower === "skill.md";
|
|
3109
|
+
});
|
|
3110
|
+
} catch {}
|
|
3111
|
+
const isEmpty = !hasPluginManifest && looseSkillFiles.length === 0;
|
|
3112
|
+
return {
|
|
3113
|
+
cwd,
|
|
3114
|
+
hasCodexDir,
|
|
3115
|
+
hasPluginManifest,
|
|
3116
|
+
hasMarketplace,
|
|
3117
|
+
looseSkillFiles,
|
|
3118
|
+
isEmpty
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
3121
|
+
var init_context2 = () => {};
|
|
3122
|
+
|
|
3123
|
+
// src/cli/commands/codex/new.ts
|
|
3124
|
+
var exports_new2 = {};
|
|
3125
|
+
__export(exports_new2, {
|
|
3126
|
+
scaffold: () => scaffold2,
|
|
3127
|
+
default: () => new_default2,
|
|
3128
|
+
decidePath: () => decidePath2
|
|
3129
|
+
});
|
|
3130
|
+
import { join as join11, basename as basename3 } from "path";
|
|
3131
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, existsSync as existsSync12 } from "fs";
|
|
3132
|
+
function decidePath2(ctx, intent, providedName) {
|
|
3133
|
+
const rawName = providedName || "";
|
|
3134
|
+
let decisionPath = "standalone";
|
|
3135
|
+
let targetDir = ctx.cwd;
|
|
3136
|
+
let shouldCreateDir = false;
|
|
3137
|
+
let migrateExisting = false;
|
|
3138
|
+
const useCurrentDirAsRoot = rawName === "." || rawName === basename3(ctx.cwd) || !rawName;
|
|
3139
|
+
if (intent === "distribute" || intent === "self-later" && ctx.looseSkillFiles.length > 0 && !ctx.hasPluginManifest) {
|
|
3140
|
+
decisionPath = "plugin";
|
|
3141
|
+
if (useCurrentDirAsRoot) {
|
|
3142
|
+
targetDir = ctx.cwd;
|
|
3143
|
+
shouldCreateDir = false;
|
|
3144
|
+
} else {
|
|
3145
|
+
targetDir = join11(ctx.cwd, rawName);
|
|
3146
|
+
shouldCreateDir = true;
|
|
3147
|
+
}
|
|
3148
|
+
migrateExisting = ctx.looseSkillFiles.length > 0;
|
|
3149
|
+
} else if (intent === "self-later" && !ctx.hasPluginManifest) {
|
|
3150
|
+
decisionPath = "plugin";
|
|
3151
|
+
if (useCurrentDirAsRoot) {
|
|
3152
|
+
targetDir = ctx.cwd;
|
|
3153
|
+
shouldCreateDir = false;
|
|
3154
|
+
} else {
|
|
3155
|
+
targetDir = join11(ctx.cwd, rawName);
|
|
3156
|
+
shouldCreateDir = true;
|
|
3157
|
+
}
|
|
3158
|
+
} else if (decisionPath === "standalone") {
|
|
3159
|
+
if (useCurrentDirAsRoot) {
|
|
3160
|
+
targetDir = ctx.cwd;
|
|
3161
|
+
shouldCreateDir = false;
|
|
3162
|
+
} else {
|
|
3163
|
+
targetDir = join11(ctx.cwd, rawName);
|
|
3164
|
+
shouldCreateDir = true;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
return { path: decisionPath, targetDir, shouldCreateDir, migrateExisting };
|
|
3168
|
+
}
|
|
3169
|
+
function scaffold2(decision, ctx, migrateContent) {
|
|
3170
|
+
const { targetDir, path, shouldCreateDir } = decision;
|
|
3171
|
+
if (existsSync12(targetDir) && shouldCreateDir) {
|
|
3172
|
+
ui.fail("Target already exists");
|
|
3173
|
+
process.exit(1);
|
|
3174
|
+
}
|
|
3175
|
+
if (shouldCreateDir) {
|
|
3176
|
+
mkdirSync3(targetDir, { recursive: true });
|
|
3177
|
+
}
|
|
3178
|
+
if (path === "plugin") {
|
|
3179
|
+
const pluginName = basename3(targetDir);
|
|
3180
|
+
const pluginJson = {
|
|
3181
|
+
name: pluginName,
|
|
3182
|
+
version: "0.1.0",
|
|
3183
|
+
description: "Scaffolded by doraval codex new",
|
|
3184
|
+
skills: "./skills/",
|
|
3185
|
+
interface: {
|
|
3186
|
+
displayName: pluginName,
|
|
3187
|
+
shortDescription: "Scaffolded starter plugin",
|
|
3188
|
+
category: "Productivity"
|
|
3189
|
+
}
|
|
3190
|
+
};
|
|
3191
|
+
mkdirSync3(join11(targetDir, ".codex-plugin"), { recursive: true });
|
|
3192
|
+
writeFileSync3(join11(targetDir, ".codex-plugin", "plugin.json"), JSON.stringify(pluginJson, null, 2));
|
|
3193
|
+
mkdirSync3(join11(targetDir, ".agents", "plugins"), { recursive: true });
|
|
3194
|
+
const marketplaceJson = {
|
|
3195
|
+
name: "local",
|
|
3196
|
+
interface: {
|
|
3197
|
+
displayName: "Local (doraval scaffold)"
|
|
3198
|
+
},
|
|
3199
|
+
plugins: [
|
|
3200
|
+
{
|
|
3201
|
+
name: pluginName,
|
|
3202
|
+
source: {
|
|
3203
|
+
source: "local",
|
|
3204
|
+
path: "../.."
|
|
3205
|
+
},
|
|
3206
|
+
policy: {
|
|
3207
|
+
installation: "AVAILABLE",
|
|
3208
|
+
authentication: "ON_INSTALL"
|
|
3209
|
+
},
|
|
3210
|
+
category: "Productivity"
|
|
3211
|
+
}
|
|
3212
|
+
]
|
|
3213
|
+
};
|
|
3214
|
+
writeFileSync3(join11(targetDir, ".agents", "plugins", "marketplace.json"), JSON.stringify(marketplaceJson, null, 2));
|
|
3215
|
+
const demoSkillName = "doraval";
|
|
3216
|
+
mkdirSync3(join11(targetDir, "skills", demoSkillName), { recursive: true });
|
|
3217
|
+
let skillContent;
|
|
3218
|
+
if (migrateContent) {
|
|
3219
|
+
skillContent = migrateContent;
|
|
3220
|
+
} else {
|
|
3221
|
+
skillContent = `---
|
|
3222
|
+
name: ${demoSkillName}
|
|
3223
|
+
description: Use doraval to validate, measure drift, and judge skills and plugins. Use when authoring or reviewing context engineering artifacts for AI coding agents (works for Codex too).
|
|
3224
|
+
---
|
|
3225
|
+
|
|
3226
|
+
# Use Doraval (Codex edition)
|
|
3227
|
+
|
|
3228
|
+
Doraval is the context engineering toolkit.
|
|
3229
|
+
|
|
3230
|
+
When you need to check a skill or Codex plugin:
|
|
3231
|
+
|
|
3232
|
+
- Validate the current directory: \`doraval validate .\`
|
|
3233
|
+
- Validate one skill: \`doraval skill validate ./skills/${demoSkillName}/\`
|
|
3234
|
+
- Check for rubric drift: \`doraval skill drift ./skills/${demoSkillName}/\`
|
|
3235
|
+
- Get an AI quality judgment: \`doraval skill judge ./skills/${demoSkillName}/\`
|
|
3236
|
+
|
|
3237
|
+
Always run \`doraval validate\` before sharing or publishing a plugin.
|
|
3238
|
+
|
|
3239
|
+
This skill demonstrates a complete, self-referential example of using doraval inside a generated Codex plugin.
|
|
3240
|
+
|
|
3241
|
+
To test in Codex:
|
|
3242
|
+
1. Make sure this plugin is listed in a marketplace (we created .agents/plugins/marketplace.json for you).
|
|
3243
|
+
2. Restart Codex.
|
|
3244
|
+
3. Open the plugin directory, select your local marketplace, and enable the plugin.
|
|
3245
|
+
4. Invoke the demo with /${pluginName}:doraval`;
|
|
3246
|
+
}
|
|
3247
|
+
writeFileSync3(join11(targetDir, "skills", demoSkillName, "SKILL.md"), skillContent);
|
|
3248
|
+
const readmePath = join11(targetDir, "README.md");
|
|
3249
|
+
if (!existsSync12(readmePath)) {
|
|
3250
|
+
writeFileSync3(readmePath, "# " + pluginName + `
|
|
3251
|
+
|
|
3252
|
+
Codex plugin scaffolded by doraval.`);
|
|
3253
|
+
}
|
|
3254
|
+
} else {
|
|
3255
|
+
mkdirSync3(join11(targetDir, "skills", "doraval"), { recursive: true });
|
|
3256
|
+
const skillBody = migrateContent || `# My Skill
|
|
3257
|
+
|
|
3258
|
+
Basic starter for Codex.`;
|
|
3259
|
+
writeFileSync3(join11(targetDir, "skills", "doraval", "SKILL.md"), `---
|
|
3260
|
+
name: doraval
|
|
3261
|
+
description: Starter (local skill)
|
|
3262
|
+
---
|
|
3263
|
+
|
|
3264
|
+
${skillBody}`);
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
var import_picocolors12, new_default2;
|
|
3268
|
+
var init_new2 = __esm(() => {
|
|
3269
|
+
init_dist();
|
|
3270
|
+
init_out();
|
|
3271
|
+
init_context2();
|
|
3272
|
+
init_prompt();
|
|
3273
|
+
import_picocolors12 = __toESM(require_picocolors(), 1);
|
|
3274
|
+
new_default2 = defineCommand({
|
|
3275
|
+
meta: {
|
|
3276
|
+
name: "new",
|
|
3277
|
+
description: "Create a new skill or plugin following Codex packaging rules"
|
|
3278
|
+
},
|
|
3279
|
+
args: {
|
|
3280
|
+
name: {
|
|
3281
|
+
type: "positional",
|
|
3282
|
+
description: "Optional name for the skill or plugin",
|
|
3283
|
+
required: false
|
|
3284
|
+
},
|
|
3285
|
+
yes: {
|
|
3286
|
+
type: "boolean",
|
|
3287
|
+
description: "Skip interactive prompts (use defaults and flags)",
|
|
3288
|
+
default: false
|
|
3289
|
+
},
|
|
3290
|
+
intent: {
|
|
3291
|
+
type: "string",
|
|
3292
|
+
description: 'Intent: "self" | "self-later" | "distribute"',
|
|
3293
|
+
required: false
|
|
3294
|
+
}
|
|
3295
|
+
},
|
|
3296
|
+
run({ args }) {
|
|
3297
|
+
ui.heading("doraval codex new \u2014 Context-aware scaffolding");
|
|
3298
|
+
const ctx = detectContext2();
|
|
3299
|
+
let intent = args.intent || "self-later";
|
|
3300
|
+
if (!args.yes) {
|
|
3301
|
+
const ans = prompt(" Intent (self | self-later | distribute)", intent);
|
|
3302
|
+
intent = ans || intent;
|
|
3303
|
+
}
|
|
3304
|
+
const decision = decidePath2(ctx, intent, args.name);
|
|
3305
|
+
ui.info(` Decision: path=${decision.path}, target=${decision.targetDir}`);
|
|
3306
|
+
let migrateContent;
|
|
3307
|
+
if (decision.migrateExisting && !args.yes) {
|
|
3308
|
+
migrateContent = "Content from your existing SKILL.md (user-confirmed).";
|
|
3309
|
+
}
|
|
3310
|
+
scaffold2(decision, ctx, migrateContent);
|
|
3311
|
+
ui.write(`
|
|
3312
|
+
${import_picocolors12.default.green("\u2713")} Created ${decision.path} at ${import_picocolors12.default.bold(decision.targetDir)}`);
|
|
3313
|
+
const cmdName = decision.path === "plugin" ? `/${basename3(decision.targetDir)}:doraval` : "/doraval (local skill)";
|
|
3314
|
+
ui.info(` Command: ${cmdName}`);
|
|
3315
|
+
if (decision.path === "plugin") {
|
|
3316
|
+
ui.info(` Codex manifest: .codex-plugin/plugin.json`);
|
|
3317
|
+
ui.info(` Marketplace catalog: .agents/plugins/marketplace.json (starter for local testing)`);
|
|
3318
|
+
ui.info(` (Move/expand the marketplace.json to $REPO_ROOT/.agents/plugins/ or ~/.agents/plugins/ as needed)`);
|
|
3319
|
+
}
|
|
3320
|
+
ui.info(` Test (local): restart Codex, select your marketplace in the plugin directory`);
|
|
3321
|
+
ui.info(` Validate: doraval validate ${decision.targetDir}`);
|
|
3322
|
+
if (decision.path === "plugin" && decision.migrateExisting) {
|
|
3323
|
+
ui.info(" (Existing content migrated where confirmed.)");
|
|
3324
|
+
}
|
|
3325
|
+
process.exit(0);
|
|
3326
|
+
}
|
|
3327
|
+
});
|
|
3328
|
+
});
|
|
3329
|
+
|
|
2766
3330
|
// src/validators/claude/skill.ts
|
|
2767
|
-
import { existsSync as
|
|
2768
|
-
import { resolve as
|
|
3331
|
+
import { existsSync as existsSync13 } from "fs";
|
|
3332
|
+
import { resolve as resolve4 } from "path";
|
|
2769
3333
|
var OPTIONAL_DIRS2, claudeSkillValidator;
|
|
2770
3334
|
var init_skill = __esm(() => {
|
|
2771
3335
|
init_frontmatter();
|
|
@@ -2777,10 +3341,10 @@ var init_skill = __esm(() => {
|
|
|
2777
3341
|
name: "Claude Skill",
|
|
2778
3342
|
description: "Validates SKILL.md per current Claude Code spec: frontmatter (name/description relaxed to recommended; directory name usually provides the /command), body, supporting files, dynamic injection (!`cmd`), substitutions ($ARGUMENTS, ${CLAUDE_*}), and advanced fields (allowed-tools, context, disable-model-invocation, when_to_use, etc.)",
|
|
2779
3343
|
detect(dir) {
|
|
2780
|
-
return
|
|
3344
|
+
return existsSync13(resolve4(dir, "SKILL.md"));
|
|
2781
3345
|
},
|
|
2782
3346
|
async validate(dir, _opts) {
|
|
2783
|
-
const skillMd =
|
|
3347
|
+
const skillMd = resolve4(dir, "SKILL.md");
|
|
2784
3348
|
const raw = await Bun.file(skillMd).text();
|
|
2785
3349
|
let parsed;
|
|
2786
3350
|
try {
|
|
@@ -2792,32 +3356,108 @@ var init_skill = __esm(() => {
|
|
|
2792
3356
|
passes: []
|
|
2793
3357
|
};
|
|
2794
3358
|
}
|
|
2795
|
-
const existingDirs = OPTIONAL_DIRS2.filter((d) =>
|
|
3359
|
+
const existingDirs = OPTIONAL_DIRS2.filter((d) => existsSync13(resolve4(dir, d)));
|
|
2796
3360
|
return validateSkillModel(parsed, { existingDirs: [...existingDirs] });
|
|
2797
3361
|
}
|
|
2798
3362
|
};
|
|
2799
3363
|
});
|
|
2800
3364
|
|
|
2801
3365
|
// src/validators/claude/plugin.ts
|
|
2802
|
-
import { existsSync as
|
|
2803
|
-
import { resolve as
|
|
2804
|
-
|
|
3366
|
+
import { existsSync as existsSync14, readdirSync as readdirSync6 } from "fs";
|
|
3367
|
+
import { resolve as resolve5, join as join12 } from "path";
|
|
3368
|
+
function levenshtein(a, b) {
|
|
3369
|
+
if (a === b)
|
|
3370
|
+
return 0;
|
|
3371
|
+
const m = a.length, n = b.length;
|
|
3372
|
+
if (m === 0)
|
|
3373
|
+
return n;
|
|
3374
|
+
if (n === 0)
|
|
3375
|
+
return m;
|
|
3376
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
3377
|
+
for (let i = 0;i <= m; i++)
|
|
3378
|
+
dp[i][0] = i;
|
|
3379
|
+
for (let j = 0;j <= n; j++)
|
|
3380
|
+
dp[0][j] = j;
|
|
3381
|
+
for (let i = 1;i <= m; i++) {
|
|
3382
|
+
for (let j = 1;j <= n; j++) {
|
|
3383
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
3384
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
return dp[m][n];
|
|
3388
|
+
}
|
|
3389
|
+
function suggestField(unknown) {
|
|
3390
|
+
const lower = unknown.toLowerCase();
|
|
3391
|
+
for (const k of KNOWN_FIELDS2) {
|
|
3392
|
+
if (k.toLowerCase() === lower)
|
|
3393
|
+
return k;
|
|
3394
|
+
if (levenshtein(k.toLowerCase(), lower) <= 1)
|
|
3395
|
+
return k;
|
|
3396
|
+
if (k.toLowerCase().startsWith(lower.slice(0, 3)) && lower.length > 3)
|
|
3397
|
+
return k;
|
|
3398
|
+
}
|
|
3399
|
+
if (lower === "licence")
|
|
3400
|
+
return "license";
|
|
3401
|
+
if (lower === "dependancies" || lower === "deps")
|
|
3402
|
+
return "dependencies";
|
|
3403
|
+
if (lower === "mcp" || lower === "mcpservers")
|
|
3404
|
+
return "mcpServers";
|
|
3405
|
+
if (lower === "lsp")
|
|
3406
|
+
return "lspServers";
|
|
3407
|
+
if (lower === "outputstyles" || lower === "styles")
|
|
3408
|
+
return "outputStyles";
|
|
3409
|
+
if (lower === "userconfig")
|
|
3410
|
+
return "userConfig";
|
|
3411
|
+
return null;
|
|
3412
|
+
}
|
|
3413
|
+
function isRelativePathLike(v) {
|
|
3414
|
+
if (typeof v !== "string")
|
|
3415
|
+
return false;
|
|
3416
|
+
return RELATIVE_PATH_REGEX.test(v) && !v.includes("..");
|
|
3417
|
+
}
|
|
3418
|
+
var NAME_REGEX2, RELATIVE_PATH_REGEX, KNOWN_FIELDS2, REPLACES_DEFAULT, claudePluginValidator;
|
|
2805
3419
|
var init_plugin = __esm(() => {
|
|
2806
3420
|
NAME_REGEX2 = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2807
3421
|
RELATIVE_PATH_REGEX = /^\.\//;
|
|
3422
|
+
KNOWN_FIELDS2 = new Set([
|
|
3423
|
+
"$schema",
|
|
3424
|
+
"name",
|
|
3425
|
+
"displayName",
|
|
3426
|
+
"version",
|
|
3427
|
+
"description",
|
|
3428
|
+
"author",
|
|
3429
|
+
"homepage",
|
|
3430
|
+
"repository",
|
|
3431
|
+
"license",
|
|
3432
|
+
"keywords",
|
|
3433
|
+
"defaultEnabled",
|
|
3434
|
+
"skills",
|
|
3435
|
+
"commands",
|
|
3436
|
+
"agents",
|
|
3437
|
+
"hooks",
|
|
3438
|
+
"mcpServers",
|
|
3439
|
+
"outputStyles",
|
|
3440
|
+
"lspServers",
|
|
3441
|
+
"experimental",
|
|
3442
|
+
"userConfig",
|
|
3443
|
+
"channels",
|
|
3444
|
+
"dependencies"
|
|
3445
|
+
]);
|
|
3446
|
+
REPLACES_DEFAULT = new Set(["commands", "agents", "outputStyles", "lspServers"]);
|
|
2808
3447
|
claudePluginValidator = {
|
|
2809
3448
|
id: "claude:plugin",
|
|
2810
3449
|
provider: "claude",
|
|
2811
3450
|
name: "Claude Plugin",
|
|
2812
|
-
description: "Validates .claude-plugin/plugin.json manifest, component
|
|
3451
|
+
description: "Validates .claude-plugin/plugin.json manifest (complete schema per Plugins reference), component path rules (replace vs augment), .claude-plugin/ purity, default dirs, single-root-skill layout, unrecognized fields + suggestions, and structure",
|
|
2813
3452
|
detect(dir) {
|
|
2814
|
-
return
|
|
3453
|
+
return existsSync14(resolve5(dir, ".claude-plugin", "plugin.json"));
|
|
2815
3454
|
},
|
|
2816
3455
|
async validate(dir, _opts) {
|
|
2817
3456
|
const errors = [];
|
|
2818
3457
|
const warnings = [];
|
|
2819
3458
|
const passes = [];
|
|
2820
|
-
const manifestPath =
|
|
3459
|
+
const manifestPath = resolve5(dir, ".claude-plugin", "plugin.json");
|
|
3460
|
+
const dotClaudePluginDir = resolve5(dir, ".claude-plugin");
|
|
2821
3461
|
let manifest;
|
|
2822
3462
|
try {
|
|
2823
3463
|
const raw = await Bun.file(manifestPath).text();
|
|
@@ -2827,6 +3467,17 @@ var init_plugin = __esm(() => {
|
|
|
2827
3467
|
errors.push(".claude-plugin/plugin.json is missing or invalid JSON");
|
|
2828
3468
|
return { errors, warnings, passes };
|
|
2829
3469
|
}
|
|
3470
|
+
try {
|
|
3471
|
+
const entries = readdirSync6(dotClaudePluginDir);
|
|
3472
|
+
const unexpected = entries.filter((e) => e !== "plugin.json");
|
|
3473
|
+
if (unexpected.length > 0) {
|
|
3474
|
+
for (const e of unexpected) {
|
|
3475
|
+
warnings.push(`Unexpected item "${e}" inside .claude-plugin/ \u2014 only plugin.json belongs here. Move component directories and files (skills/, commands/, agents/, hooks/, .mcp.json etc.) to the plugin root.`);
|
|
3476
|
+
}
|
|
3477
|
+
} else if (entries.length === 1) {
|
|
3478
|
+
passes.push(".claude-plugin/ contains only plugin.json (correct layout)");
|
|
3479
|
+
}
|
|
3480
|
+
} catch {}
|
|
2830
3481
|
if (!manifest.name) {
|
|
2831
3482
|
errors.push('Missing required field: "name"');
|
|
2832
3483
|
} else {
|
|
@@ -2840,10 +3491,12 @@ var init_plugin = __esm(() => {
|
|
|
2840
3491
|
if (manifest.version !== undefined) {
|
|
2841
3492
|
const v = String(manifest.version);
|
|
2842
3493
|
if (!/^\d+\.\d+\.\d+/.test(v)) {
|
|
2843
|
-
errors.push(`Invalid version format: "${v}" \u2014 must
|
|
3494
|
+
errors.push(`Invalid version format: "${v}" \u2014 must look like semver (MAJOR.MINOR.PATCH) when using explicit versioning`);
|
|
2844
3495
|
} else {
|
|
2845
|
-
passes.push(`version: "${v}"`);
|
|
3496
|
+
passes.push(`version: "${v}" (explicit \u2014 bump on every release to publish updates)`);
|
|
2846
3497
|
}
|
|
3498
|
+
} else {
|
|
3499
|
+
passes.push("version omitted (git commit SHA used as version key \u2014 every commit becomes an available update)");
|
|
2847
3500
|
}
|
|
2848
3501
|
if (manifest.description !== undefined) {
|
|
2849
3502
|
const desc = String(manifest.description);
|
|
@@ -2852,65 +3505,172 @@ var init_plugin = __esm(() => {
|
|
|
2852
3505
|
} else {
|
|
2853
3506
|
passes.push("description field present");
|
|
2854
3507
|
}
|
|
3508
|
+
} else {
|
|
3509
|
+
warnings.push('Missing "description" (recommended for UI, marketplace listings, and auto-discovery)');
|
|
2855
3510
|
}
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
3511
|
+
if (manifest.displayName !== undefined) {
|
|
3512
|
+
passes.push(`displayName: "${manifest.displayName}" (human UI label; falls back to name)`);
|
|
3513
|
+
}
|
|
3514
|
+
if (manifest.author !== undefined) {
|
|
3515
|
+
const a = manifest.author;
|
|
3516
|
+
if (a && typeof a === "object" && a.name) {
|
|
3517
|
+
passes.push("author present");
|
|
3518
|
+
} else {
|
|
3519
|
+
warnings.push('author should be an object like {"name": "...", "email?": "..."}');
|
|
3520
|
+
}
|
|
3521
|
+
}
|
|
3522
|
+
if (manifest.license !== undefined) {
|
|
3523
|
+
passes.push(`license: "${manifest.license}"`);
|
|
3524
|
+
}
|
|
3525
|
+
if (manifest.keywords !== undefined) {
|
|
3526
|
+
if (Array.isArray(manifest.keywords)) {
|
|
3527
|
+
passes.push(`keywords: [${manifest.keywords.join(", ")}]`);
|
|
3528
|
+
} else {
|
|
3529
|
+
errors.push("keywords must be an array of strings");
|
|
3530
|
+
}
|
|
3531
|
+
}
|
|
3532
|
+
if (manifest.defaultEnabled !== undefined) {
|
|
3533
|
+
passes.push(`defaultEnabled: ${manifest.defaultEnabled}`);
|
|
3534
|
+
}
|
|
3535
|
+
if (manifest.homepage)
|
|
3536
|
+
passes.push("homepage present");
|
|
3537
|
+
if (manifest.repository)
|
|
3538
|
+
passes.push("repository present");
|
|
3539
|
+
const unknown = Object.keys(manifest).filter((k) => !KNOWN_FIELDS2.has(k));
|
|
3540
|
+
for (const k of unknown) {
|
|
3541
|
+
const sug = suggestField(k);
|
|
3542
|
+
const hint = sug ? ` (did you mean "${sug}"?)` : "";
|
|
3543
|
+
warnings.push(`Unrecognized top-level field "${k}"${hint} \u2014 will be ignored at runtime (allowed for cross-tool manifest compatibility).`);
|
|
3544
|
+
}
|
|
3545
|
+
const handleField = (field, val) => {
|
|
3546
|
+
if (val === undefined || val === null)
|
|
3547
|
+
return;
|
|
3548
|
+
if (isRelativePathLike(val) || Array.isArray(val) && val.every(isRelativePathLike)) {
|
|
3549
|
+
const arr = Array.isArray(val) ? val : [val];
|
|
3550
|
+
for (const p of arr) {
|
|
3551
|
+
const s = String(p);
|
|
3552
|
+
if (!RELATIVE_PATH_REGEX.test(s)) {
|
|
3553
|
+
errors.push(`${field}: path "${s}" must start with "./"`);
|
|
3554
|
+
} else if (s.includes("..")) {
|
|
3555
|
+
errors.push(`${field}: path "${s}" must not use ".." (paths are confined to the plugin tree after cache copy)`);
|
|
3556
|
+
} else if (existsSync14(resolve5(dir, s))) {
|
|
3557
|
+
passes.push(`${field}: path "${s}" exists`);
|
|
3558
|
+
} else {
|
|
3559
|
+
warnings.push(`${field}: path "${s}" does not exist on disk`);
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
if (field === "skills") {
|
|
3563
|
+
passes.push(`${field}: augments the default skills/ (both are scanned)`);
|
|
3564
|
+
} else if (REPLACES_DEFAULT.has(field)) {
|
|
3565
|
+
passes.push(`${field}: custom path replaces default ${field}/ scan`);
|
|
2866
3566
|
} else {
|
|
2867
|
-
|
|
3567
|
+
passes.push(`${field}: custom path or config (merge rules apply)`);
|
|
2868
3568
|
}
|
|
3569
|
+
} else if (typeof val === "object") {
|
|
3570
|
+
passes.push(`${field}: inline ${field} config present`);
|
|
2869
3571
|
}
|
|
2870
3572
|
};
|
|
2871
|
-
|
|
2872
|
-
if (manifest[
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
3573
|
+
["skills", "commands", "agents", "hooks", "mcpServers", "outputStyles", "lspServers"].forEach((f) => {
|
|
3574
|
+
if (manifest[f] !== undefined)
|
|
3575
|
+
handleField(f, manifest[f]);
|
|
3576
|
+
});
|
|
3577
|
+
if (manifest.experimental && typeof manifest.experimental === "object") {
|
|
3578
|
+
const exp = manifest.experimental;
|
|
3579
|
+
if (exp.themes !== undefined)
|
|
3580
|
+
handleField("experimental.themes", exp.themes);
|
|
3581
|
+
if (exp.monitors !== undefined)
|
|
3582
|
+
handleField("experimental.monitors", exp.monitors);
|
|
3583
|
+
passes.push("experimental section present (themes and monitors are experimental components)");
|
|
3584
|
+
}
|
|
3585
|
+
if (manifest.userConfig && typeof manifest.userConfig === "object") {
|
|
3586
|
+
const keys = Object.keys(manifest.userConfig);
|
|
3587
|
+
passes.push(`userConfig: ${keys.length} user-configurable value(s) declared`);
|
|
3588
|
+
for (const k of keys) {
|
|
3589
|
+
const opt = manifest.userConfig[k];
|
|
3590
|
+
if (!opt || !opt.type || !opt.title) {
|
|
3591
|
+
warnings.push(`userConfig.${k} is missing required "type" and/or "title"`);
|
|
3592
|
+
}
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
if (Array.isArray(manifest.channels)) {
|
|
3596
|
+
passes.push(`channels: ${manifest.channels.length} channel(s) (each binds to an mcpServer)`);
|
|
3597
|
+
manifest.channels.forEach((ch, i) => {
|
|
3598
|
+
if (!ch?.server)
|
|
3599
|
+
warnings.push(`channels[${i}]: "server" is required and must match an mcpServers key`);
|
|
3600
|
+
});
|
|
3601
|
+
}
|
|
3602
|
+
if (Array.isArray(manifest.dependencies)) {
|
|
3603
|
+
passes.push(`dependencies: declares ${manifest.dependencies.length} plugin dependency/ies`);
|
|
3604
|
+
}
|
|
3605
|
+
const skillsDir = resolve5(dir, "skills");
|
|
3606
|
+
if (existsSync14(skillsDir)) {
|
|
3607
|
+
const entries = readdirSync6(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
3608
|
+
for (const e of entries) {
|
|
3609
|
+
const md = join12(skillsDir, e.name, "SKILL.md");
|
|
3610
|
+
if (existsSync14(md)) {
|
|
3611
|
+
passes.push(`skills/${e.name}/SKILL.md exists`);
|
|
2883
3612
|
} else {
|
|
2884
|
-
errors.push(`skills/${
|
|
3613
|
+
errors.push(`skills/${e.name}/ is missing SKILL.md`);
|
|
2885
3614
|
}
|
|
2886
3615
|
}
|
|
3616
|
+
if (manifest.skills !== undefined) {
|
|
3617
|
+
warnings.push('Default skills/ dir co-exists with manifest "skills" \u2014 manifest path is authoritative; default folder ignored for loading');
|
|
3618
|
+
}
|
|
2887
3619
|
}
|
|
2888
|
-
const commandsDir =
|
|
2889
|
-
if (
|
|
2890
|
-
const
|
|
2891
|
-
if (
|
|
2892
|
-
passes.push(`commands/ has ${
|
|
2893
|
-
}
|
|
2894
|
-
|
|
3620
|
+
const commandsDir = resolve5(dir, "commands");
|
|
3621
|
+
if (existsSync14(commandsDir)) {
|
|
3622
|
+
const mds = readdirSync6(commandsDir).filter((f) => f.endsWith(".md"));
|
|
3623
|
+
if (mds.length) {
|
|
3624
|
+
passes.push(`commands/ has ${mds.length} .md file(s)`);
|
|
3625
|
+
}
|
|
3626
|
+
if (manifest.commands !== undefined) {
|
|
3627
|
+
warnings.push('commands/ co-exists with manifest "commands" \u2014 manifest replaces default (dir ignored)');
|
|
2895
3628
|
}
|
|
2896
3629
|
}
|
|
2897
|
-
const agentsDir =
|
|
2898
|
-
if (
|
|
2899
|
-
const
|
|
2900
|
-
if (
|
|
2901
|
-
passes.push(`agents/ has ${
|
|
2902
|
-
}
|
|
2903
|
-
|
|
3630
|
+
const agentsDir = resolve5(dir, "agents");
|
|
3631
|
+
if (existsSync14(agentsDir)) {
|
|
3632
|
+
const mds = readdirSync6(agentsDir).filter((f) => f.endsWith(".md"));
|
|
3633
|
+
if (mds.length) {
|
|
3634
|
+
passes.push(`agents/ has ${mds.length} .md file(s)`);
|
|
3635
|
+
}
|
|
3636
|
+
if (manifest.agents !== undefined) {
|
|
3637
|
+
warnings.push('agents/ co-exists with manifest "agents" \u2014 manifest replaces default (dir ignored)');
|
|
2904
3638
|
}
|
|
2905
3639
|
}
|
|
3640
|
+
if (existsSync14(resolve5(dir, "output-styles"))) {
|
|
3641
|
+
passes.push("output-styles/ directory present");
|
|
3642
|
+
if (manifest.outputStyles)
|
|
3643
|
+
warnings.push("output-styles/ co-exists with manifest outputStyles \u2014 manifest wins");
|
|
3644
|
+
}
|
|
3645
|
+
if (existsSync14(resolve5(dir, "themes")))
|
|
3646
|
+
passes.push("themes/ present (experimental)");
|
|
3647
|
+
if (existsSync14(resolve5(dir, "monitors")) || manifest.experimental?.monitors) {
|
|
3648
|
+
passes.push("monitors config present (experimental)");
|
|
3649
|
+
}
|
|
3650
|
+
if (existsSync14(resolve5(dir, "bin")))
|
|
3651
|
+
passes.push("bin/ present (adds executables to Bash tool $PATH)");
|
|
3652
|
+
if (existsSync14(resolve5(dir, "settings.json")))
|
|
3653
|
+
passes.push("settings.json present (plugin defaults for agent/statusline)");
|
|
3654
|
+
if (existsSync14(resolve5(dir, "README.md")))
|
|
3655
|
+
passes.push("README.md present");
|
|
3656
|
+
if (existsSync14(resolve5(dir, ".mcp.json")))
|
|
3657
|
+
passes.push(".mcp.json present (validated by claude:mcp)");
|
|
3658
|
+
if (existsSync14(resolve5(dir, ".lsp.json")))
|
|
3659
|
+
passes.push(".lsp.json present (validated by claude:lsp when registered)");
|
|
3660
|
+
if (existsSync14(resolve5(dir, "hooks/hooks.json")) || existsSync14(resolve5(dir, "hooks.json"))) {
|
|
3661
|
+
passes.push("hooks config present (validated by claude:hooks)");
|
|
3662
|
+
}
|
|
3663
|
+
if (existsSync14(resolve5(dir, "SKILL.md")) && !existsSync14(skillsDir) && manifest.skills === undefined) {
|
|
3664
|
+
passes.push('Root SKILL.md detected \u2014 plugin will be treated as a single-skill plugin (prefer frontmatter "name" for stable /command)');
|
|
3665
|
+
}
|
|
2906
3666
|
return { errors, warnings, passes };
|
|
2907
3667
|
}
|
|
2908
3668
|
};
|
|
2909
3669
|
});
|
|
2910
3670
|
|
|
2911
3671
|
// src/validators/claude/marketplace.ts
|
|
2912
|
-
import { existsSync as
|
|
2913
|
-
import { resolve as
|
|
3672
|
+
import { existsSync as existsSync15, readdirSync as readdirSync7 } from "fs";
|
|
3673
|
+
import { resolve as resolve6, join as join13 } from "path";
|
|
2914
3674
|
var claudeMarketplaceValidator;
|
|
2915
3675
|
var init_marketplace = __esm(() => {
|
|
2916
3676
|
claudeMarketplaceValidator = {
|
|
@@ -2919,16 +3679,16 @@ var init_marketplace = __esm(() => {
|
|
|
2919
3679
|
name: "Claude Plugin Marketplace",
|
|
2920
3680
|
description: "Validates marketplace structure: plugins/ directory with valid plugin subdirectories",
|
|
2921
3681
|
detect(dir) {
|
|
2922
|
-
const pluginsDir =
|
|
2923
|
-
if (!
|
|
3682
|
+
const pluginsDir = resolve6(dir, "plugins");
|
|
3683
|
+
if (!existsSync15(pluginsDir))
|
|
2924
3684
|
return false;
|
|
2925
3685
|
try {
|
|
2926
|
-
const entries =
|
|
3686
|
+
const entries = readdirSync7(pluginsDir, { withFileTypes: true });
|
|
2927
3687
|
for (const entry of entries) {
|
|
2928
3688
|
if (!entry.isDirectory())
|
|
2929
3689
|
continue;
|
|
2930
|
-
const hasSkills =
|
|
2931
|
-
const hasManifest =
|
|
3690
|
+
const hasSkills = existsSync15(join13(pluginsDir, entry.name, "skills"));
|
|
3691
|
+
const hasManifest = existsSync15(join13(pluginsDir, entry.name, ".claude-plugin", "plugin.json"));
|
|
2932
3692
|
if (hasSkills || hasManifest)
|
|
2933
3693
|
return true;
|
|
2934
3694
|
}
|
|
@@ -2939,33 +3699,33 @@ var init_marketplace = __esm(() => {
|
|
|
2939
3699
|
const errors = [];
|
|
2940
3700
|
const warnings = [];
|
|
2941
3701
|
const passes = [];
|
|
2942
|
-
const pluginsDir =
|
|
2943
|
-
if (!
|
|
3702
|
+
const pluginsDir = resolve6(dir, "plugins");
|
|
3703
|
+
if (!existsSync15(pluginsDir)) {
|
|
2944
3704
|
errors.push("Missing plugins/ directory");
|
|
2945
3705
|
return { errors, warnings, passes };
|
|
2946
3706
|
}
|
|
2947
3707
|
passes.push("plugins/ directory exists");
|
|
2948
|
-
const pluginEntries =
|
|
3708
|
+
const pluginEntries = readdirSync7(pluginsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
2949
3709
|
if (pluginEntries.length === 0) {
|
|
2950
3710
|
errors.push("plugins/ directory is empty \u2014 expected at least one plugin");
|
|
2951
3711
|
return { errors, warnings, passes };
|
|
2952
3712
|
}
|
|
2953
3713
|
passes.push(`${pluginEntries.length} plugin(s) found`);
|
|
2954
|
-
if (
|
|
3714
|
+
if (existsSync15(resolve6(dir, "README.md"))) {
|
|
2955
3715
|
passes.push("README.md exists at marketplace root");
|
|
2956
3716
|
} else {
|
|
2957
3717
|
warnings.push("No README.md at marketplace root \u2014 recommended for discoverability");
|
|
2958
3718
|
}
|
|
2959
|
-
if (
|
|
3719
|
+
if (existsSync15(resolve6(dir, "LICENSE"))) {
|
|
2960
3720
|
passes.push("LICENSE exists at marketplace root");
|
|
2961
3721
|
} else {
|
|
2962
3722
|
warnings.push("No LICENSE at marketplace root \u2014 recommended");
|
|
2963
3723
|
}
|
|
2964
3724
|
for (const plugin of pluginEntries) {
|
|
2965
|
-
const pluginPath =
|
|
2966
|
-
const hasSkills =
|
|
2967
|
-
const hasManifest =
|
|
2968
|
-
const hasReadme =
|
|
3725
|
+
const pluginPath = join13(pluginsDir, plugin.name);
|
|
3726
|
+
const hasSkills = existsSync15(join13(pluginPath, "skills"));
|
|
3727
|
+
const hasManifest = existsSync15(join13(pluginPath, ".claude-plugin", "plugin.json"));
|
|
3728
|
+
const hasReadme = existsSync15(join13(pluginPath, "README.md"));
|
|
2969
3729
|
if (hasManifest || hasSkills) {
|
|
2970
3730
|
passes.push(`Plugin "${plugin.name}" has ${hasManifest ? "manifest" : "skills/"}`);
|
|
2971
3731
|
} else {
|
|
@@ -2981,35 +3741,55 @@ var init_marketplace = __esm(() => {
|
|
|
2981
3741
|
});
|
|
2982
3742
|
|
|
2983
3743
|
// src/validators/claude/hooks.ts
|
|
2984
|
-
import { existsSync as
|
|
2985
|
-
import { resolve as
|
|
3744
|
+
import { existsSync as existsSync16 } from "fs";
|
|
3745
|
+
import { resolve as resolve7 } from "path";
|
|
2986
3746
|
var KNOWN_EVENTS, claudeHooksValidator;
|
|
2987
3747
|
var init_hooks = __esm(() => {
|
|
2988
3748
|
KNOWN_EVENTS = [
|
|
3749
|
+
"SessionStart",
|
|
3750
|
+
"Setup",
|
|
3751
|
+
"UserPromptSubmit",
|
|
3752
|
+
"UserPromptExpansion",
|
|
2989
3753
|
"PreToolUse",
|
|
3754
|
+
"PermissionRequest",
|
|
3755
|
+
"PermissionDenied",
|
|
2990
3756
|
"PostToolUse",
|
|
2991
|
-
"
|
|
3757
|
+
"PostToolUseFailure",
|
|
3758
|
+
"PostToolBatch",
|
|
3759
|
+
"Notification",
|
|
3760
|
+
"MessageDisplay",
|
|
3761
|
+
"SubagentStart",
|
|
2992
3762
|
"SubagentStop",
|
|
2993
|
-
"
|
|
2994
|
-
"
|
|
2995
|
-
"
|
|
3763
|
+
"TaskCreated",
|
|
3764
|
+
"TaskCompleted",
|
|
3765
|
+
"Stop",
|
|
3766
|
+
"StopFailure",
|
|
3767
|
+
"TeammateIdle",
|
|
3768
|
+
"InstructionsLoaded",
|
|
3769
|
+
"ConfigChange",
|
|
3770
|
+
"CwdChanged",
|
|
3771
|
+
"FileChanged",
|
|
3772
|
+
"WorktreeCreate",
|
|
3773
|
+
"WorktreeRemove",
|
|
2996
3774
|
"PreCompact",
|
|
2997
|
-
"
|
|
2998
|
-
"
|
|
3775
|
+
"PostCompact",
|
|
3776
|
+
"Elicitation",
|
|
3777
|
+
"ElicitationResult",
|
|
3778
|
+
"SessionEnd"
|
|
2999
3779
|
];
|
|
3000
3780
|
claudeHooksValidator = {
|
|
3001
3781
|
id: "claude:hooks",
|
|
3002
3782
|
provider: "claude",
|
|
3003
3783
|
name: "Claude Hooks",
|
|
3004
|
-
description: "Validates hooks/hooks.json:
|
|
3784
|
+
description: "Validates hooks/hooks.json (or root hooks.json): all lifecycle events per Plugins reference, hook group structure (matcher + hooks[]), supported hook types (command, http, mcp_tool, prompt, agent)",
|
|
3005
3785
|
detect(dir) {
|
|
3006
|
-
return
|
|
3786
|
+
return existsSync16(resolve7(dir, "hooks", "hooks.json")) || existsSync16(resolve7(dir, "hooks.json"));
|
|
3007
3787
|
},
|
|
3008
3788
|
async validate(dir, _opts) {
|
|
3009
3789
|
const errors = [];
|
|
3010
3790
|
const warnings = [];
|
|
3011
3791
|
const passes = [];
|
|
3012
|
-
const hooksPath =
|
|
3792
|
+
const hooksPath = existsSync16(resolve7(dir, "hooks", "hooks.json")) ? resolve7(dir, "hooks", "hooks.json") : resolve7(dir, "hooks.json");
|
|
3013
3793
|
let config;
|
|
3014
3794
|
try {
|
|
3015
3795
|
const raw = await Bun.file(hooksPath).text();
|
|
@@ -3024,32 +3804,74 @@ var init_hooks = __esm(() => {
|
|
|
3024
3804
|
if (KNOWN_EVENTS.includes(name)) {
|
|
3025
3805
|
passes.push(`Event "${name}" is a known lifecycle event`);
|
|
3026
3806
|
} else {
|
|
3027
|
-
warnings.push(`Unknown event name: "${name}" \u2014
|
|
3807
|
+
warnings.push(`Unknown event name: "${name}" \u2014 see full list in Plugins reference (SessionStart, PreToolUse, PostToolUse, Stop, ...)`);
|
|
3028
3808
|
}
|
|
3029
3809
|
}
|
|
3810
|
+
for (const [event, groups] of Object.entries(config)) {
|
|
3811
|
+
if (!Array.isArray(groups)) {
|
|
3812
|
+
errors.push(`Event "${event}": value must be an array of hook groups`);
|
|
3813
|
+
continue;
|
|
3814
|
+
}
|
|
3815
|
+
groups.forEach((group, gi) => {
|
|
3816
|
+
if (!group || typeof group !== "object") {
|
|
3817
|
+
errors.push(`${event}[${gi}]: hook group must be an object`);
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3820
|
+
if (group.matcher !== undefined && typeof group.matcher !== "string") {
|
|
3821
|
+
warnings.push(`${event}[${gi}]: "matcher" should be a string (e.g. "Write|Edit" or glob)`);
|
|
3822
|
+
}
|
|
3823
|
+
const hooksArr = group.hooks;
|
|
3824
|
+
if (!Array.isArray(hooksArr)) {
|
|
3825
|
+
errors.push(`${event}[${gi}]: missing or invalid "hooks" array`);
|
|
3826
|
+
return;
|
|
3827
|
+
}
|
|
3828
|
+
hooksArr.forEach((h, hi) => {
|
|
3829
|
+
if (!h || typeof h !== "object" || !h.type) {
|
|
3830
|
+
errors.push(`${event}[${gi}].hooks[${hi}]: must have "type"`);
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
3833
|
+
const t = String(h.type);
|
|
3834
|
+
if (!["command", "http", "mcp_tool", "prompt", "agent"].includes(t)) {
|
|
3835
|
+
warnings.push(`${event}[${gi}].hooks[${hi}]: unknown type "${t}" (valid: command, http, mcp_tool, prompt, agent)`);
|
|
3836
|
+
}
|
|
3837
|
+
if (t === "command" && !h.command) {
|
|
3838
|
+
errors.push(`${event}[${gi}].hooks[${hi}]: type=command requires "command"`);
|
|
3839
|
+
}
|
|
3840
|
+
if (t === "http" && !h.url) {
|
|
3841
|
+
errors.push(`${event}[${gi}].hooks[${hi}]: type=http requires "url"`);
|
|
3842
|
+
}
|
|
3843
|
+
if (h.command && typeof h.command === "string" && /\$\{CLAUDE_/.test(h.command)) {
|
|
3844
|
+
passes.push(`${event}[${gi}].hooks[${hi}]: uses plugin env substitution`);
|
|
3845
|
+
}
|
|
3846
|
+
});
|
|
3847
|
+
if (hooksArr.length > 0) {
|
|
3848
|
+
passes.push(`Event "${event}" has ${hooksArr.length} hook action(s)`);
|
|
3849
|
+
}
|
|
3850
|
+
});
|
|
3851
|
+
}
|
|
3030
3852
|
return { errors, warnings, passes };
|
|
3031
3853
|
}
|
|
3032
3854
|
};
|
|
3033
3855
|
});
|
|
3034
3856
|
|
|
3035
3857
|
// src/validators/claude/mcp.ts
|
|
3036
|
-
import { existsSync as
|
|
3037
|
-
import { resolve as
|
|
3858
|
+
import { existsSync as existsSync17 } from "fs";
|
|
3859
|
+
import { resolve as resolve8 } from "path";
|
|
3038
3860
|
var claudeMcpValidator;
|
|
3039
3861
|
var init_mcp = __esm(() => {
|
|
3040
3862
|
claudeMcpValidator = {
|
|
3041
3863
|
id: "claude:mcp",
|
|
3042
3864
|
provider: "claude",
|
|
3043
3865
|
name: "Claude MCP Config",
|
|
3044
|
-
description: "Validates .mcp.json: server
|
|
3866
|
+
description: "Validates .mcp.json (or inline via plugin.json mcpServers): server entries (stdio: command+args, or url), env, cwd, ${CLAUDE_PLUGIN_ROOT} etc. substitutions per Plugins reference",
|
|
3045
3867
|
detect(dir) {
|
|
3046
|
-
return
|
|
3868
|
+
return existsSync17(resolve8(dir, ".mcp.json"));
|
|
3047
3869
|
},
|
|
3048
3870
|
async validate(dir, _opts) {
|
|
3049
3871
|
const errors = [];
|
|
3050
3872
|
const warnings = [];
|
|
3051
3873
|
const passes = [];
|
|
3052
|
-
const mcpPath =
|
|
3874
|
+
const mcpPath = resolve8(dir, ".mcp.json");
|
|
3053
3875
|
let config;
|
|
3054
3876
|
try {
|
|
3055
3877
|
const raw = await Bun.file(mcpPath).text();
|
|
@@ -3069,14 +3891,42 @@ var init_mcp = __esm(() => {
|
|
|
3069
3891
|
return { errors, warnings, passes };
|
|
3070
3892
|
}
|
|
3071
3893
|
passes.push(`${serverNames.length} server(s) defined`);
|
|
3894
|
+
for (const [name, entry] of Object.entries(config)) {
|
|
3895
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
3896
|
+
errors.push(`mcp server "${name}": definition must be an object`);
|
|
3897
|
+
continue;
|
|
3898
|
+
}
|
|
3899
|
+
const e = entry;
|
|
3900
|
+
const hasCommand = typeof e.command === "string";
|
|
3901
|
+
const hasUrl = typeof e.url === "string";
|
|
3902
|
+
if (!hasCommand && !hasUrl) {
|
|
3903
|
+
errors.push(`mcp server "${name}": must have either "command" (for stdio) or "url" (for SSE/HTTP)`);
|
|
3904
|
+
}
|
|
3905
|
+
if (hasCommand && !Array.isArray(e.args)) {
|
|
3906
|
+
warnings.push(`mcp server "${name}": "command" present but no "args" array (ok for some servers)`);
|
|
3907
|
+
}
|
|
3908
|
+
if (hasUrl && hasCommand) {
|
|
3909
|
+
warnings.push(`mcp server "${name}": both "command" and "url" present \u2014 usually one or the other`);
|
|
3910
|
+
}
|
|
3911
|
+
if (e.env && typeof e.env === "object") {
|
|
3912
|
+
passes.push(`mcp server "${name}": has env`);
|
|
3913
|
+
}
|
|
3914
|
+
if (typeof e.cwd === "string") {
|
|
3915
|
+
passes.push(`mcp server "${name}": has cwd`);
|
|
3916
|
+
}
|
|
3917
|
+
const hasSubs = JSON.stringify(e).match(/\$\{CLAUDE_PLUGIN_(ROOT|DATA)|CLAUDE_PROJECT_DIR|user_config\.|ENV_VAR\}/);
|
|
3918
|
+
if (hasSubs) {
|
|
3919
|
+
passes.push(`mcp server "${name}": uses \${CLAUDE_PLUGIN_*} / user_config / env substitution`);
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3072
3922
|
return { errors, warnings, passes };
|
|
3073
3923
|
}
|
|
3074
3924
|
};
|
|
3075
3925
|
});
|
|
3076
3926
|
|
|
3077
3927
|
// src/validators/claude/subagent.ts
|
|
3078
|
-
import { existsSync as
|
|
3079
|
-
import { resolve as
|
|
3928
|
+
import { existsSync as existsSync18, readdirSync as readdirSync8 } from "fs";
|
|
3929
|
+
import { resolve as resolve9, join as join14 } from "path";
|
|
3080
3930
|
var claudeSubagentValidator;
|
|
3081
3931
|
var init_subagent = __esm(() => {
|
|
3082
3932
|
init_frontmatter();
|
|
@@ -3084,13 +3934,13 @@ var init_subagent = __esm(() => {
|
|
|
3084
3934
|
id: "claude:subagent",
|
|
3085
3935
|
provider: "claude",
|
|
3086
3936
|
name: "Claude Subagents",
|
|
3087
|
-
description: "Validates agents
|
|
3937
|
+
description: "Validates agents/*.md (plugin subagents): frontmatter per spec (name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, isolation=worktree), body; warns on disallowed fields (hooks, mcpServers, permissionMode) for security",
|
|
3088
3938
|
detect(dir) {
|
|
3089
|
-
const agentsDir =
|
|
3090
|
-
if (!
|
|
3939
|
+
const agentsDir = resolve9(dir, "agents");
|
|
3940
|
+
if (!existsSync18(agentsDir))
|
|
3091
3941
|
return false;
|
|
3092
3942
|
try {
|
|
3093
|
-
return
|
|
3943
|
+
return readdirSync8(agentsDir).some((f) => f.endsWith(".md"));
|
|
3094
3944
|
} catch {
|
|
3095
3945
|
return false;
|
|
3096
3946
|
}
|
|
@@ -3099,27 +3949,63 @@ var init_subagent = __esm(() => {
|
|
|
3099
3949
|
const errors = [];
|
|
3100
3950
|
const warnings = [];
|
|
3101
3951
|
const passes = [];
|
|
3102
|
-
const agentsDir =
|
|
3103
|
-
const mdFiles =
|
|
3952
|
+
const agentsDir = resolve9(dir, "agents");
|
|
3953
|
+
const mdFiles = readdirSync8(agentsDir).filter((f) => f.endsWith(".md"));
|
|
3104
3954
|
if (mdFiles.length === 0) {
|
|
3105
3955
|
errors.push("agents/ directory has no .md files");
|
|
3106
3956
|
return { errors, warnings, passes };
|
|
3107
3957
|
}
|
|
3108
3958
|
passes.push(`${mdFiles.length} agent definition(s) found`);
|
|
3959
|
+
const SUPPORTED = new Set([
|
|
3960
|
+
"name",
|
|
3961
|
+
"description",
|
|
3962
|
+
"model",
|
|
3963
|
+
"effort",
|
|
3964
|
+
"maxTurns",
|
|
3965
|
+
"tools",
|
|
3966
|
+
"disallowedTools",
|
|
3967
|
+
"skills",
|
|
3968
|
+
"memory",
|
|
3969
|
+
"background",
|
|
3970
|
+
"isolation"
|
|
3971
|
+
]);
|
|
3972
|
+
const DISALLOWED = new Set(["hooks", "mcpServers", "permissionMode"]);
|
|
3109
3973
|
for (const file of mdFiles) {
|
|
3110
|
-
const filePath =
|
|
3974
|
+
const filePath = join14(agentsDir, file);
|
|
3111
3975
|
const raw = await Bun.file(filePath).text();
|
|
3112
3976
|
try {
|
|
3113
3977
|
const parsed = parseFrontmatter(raw);
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
warnings.push(`${file}: missing "description" in frontmatter`);
|
|
3978
|
+
const fm = parsed.data;
|
|
3979
|
+
if (Object.keys(fm).length === 0) {
|
|
3980
|
+
warnings.push(`${file}: no YAML frontmatter (description recommended so Claude knows when to invoke)`);
|
|
3118
3981
|
} else {
|
|
3119
|
-
|
|
3982
|
+
if (fm.description) {
|
|
3983
|
+
passes.push(`${file}: has frontmatter with description`);
|
|
3984
|
+
} else {
|
|
3985
|
+
warnings.push(`${file}: missing "description" in frontmatter`);
|
|
3986
|
+
}
|
|
3987
|
+
const usedSupported = [];
|
|
3988
|
+
Object.keys(fm).forEach((k) => {
|
|
3989
|
+
if (SUPPORTED.has(k))
|
|
3990
|
+
usedSupported.push(k);
|
|
3991
|
+
if (DISALLOWED.has(k)) {
|
|
3992
|
+
errors.push(`${file}: frontmatter "${k}" is not supported for plugin-shipped agents (security restriction)`);
|
|
3993
|
+
}
|
|
3994
|
+
});
|
|
3995
|
+
if (usedSupported.length) {
|
|
3996
|
+
passes.push(`${file}: frontmatter fields: ${usedSupported.join(", ")}`);
|
|
3997
|
+
}
|
|
3998
|
+
if (fm.isolation !== undefined && fm.isolation !== "worktree") {
|
|
3999
|
+
errors.push(`${file}: "isolation" must be "worktree" if present (only supported value for plugin agents)`);
|
|
4000
|
+
}
|
|
4001
|
+
if (fm.name && typeof fm.name === "string") {
|
|
4002
|
+
passes.push(`${file}: name: "${fm.name}"`);
|
|
4003
|
+
}
|
|
3120
4004
|
}
|
|
3121
4005
|
if (!parsed.content.trim()) {
|
|
3122
4006
|
errors.push(`${file}: body is empty`);
|
|
4007
|
+
} else {
|
|
4008
|
+
passes.push(`${file}: has agent system prompt body`);
|
|
3123
4009
|
}
|
|
3124
4010
|
} catch {
|
|
3125
4011
|
errors.push(`${file}: failed to parse`);
|
|
@@ -3131,8 +4017,8 @@ var init_subagent = __esm(() => {
|
|
|
3131
4017
|
});
|
|
3132
4018
|
|
|
3133
4019
|
// src/validators/claude/command.ts
|
|
3134
|
-
import { existsSync as
|
|
3135
|
-
import { resolve as
|
|
4020
|
+
import { existsSync as existsSync19, readdirSync as readdirSync9 } from "fs";
|
|
4021
|
+
import { resolve as resolve10, join as join15 } from "path";
|
|
3136
4022
|
var claudeCommandValidator;
|
|
3137
4023
|
var init_command = __esm(() => {
|
|
3138
4024
|
init_frontmatter();
|
|
@@ -3142,11 +4028,11 @@ var init_command = __esm(() => {
|
|
|
3142
4028
|
name: "Claude Commands",
|
|
3143
4029
|
description: "Validates commands/ (or legacy .claude/commands/) .md files: frontmatter (including rich skill fields), description, body",
|
|
3144
4030
|
detect(dir) {
|
|
3145
|
-
const commandsDir =
|
|
3146
|
-
if (!
|
|
4031
|
+
const commandsDir = resolve10(dir, "commands");
|
|
4032
|
+
if (!existsSync19(commandsDir))
|
|
3147
4033
|
return false;
|
|
3148
4034
|
try {
|
|
3149
|
-
return
|
|
4035
|
+
return readdirSync9(commandsDir).some((f) => f.endsWith(".md"));
|
|
3150
4036
|
} catch {
|
|
3151
4037
|
return false;
|
|
3152
4038
|
}
|
|
@@ -3155,15 +4041,15 @@ var init_command = __esm(() => {
|
|
|
3155
4041
|
const errors = [];
|
|
3156
4042
|
const warnings = [];
|
|
3157
4043
|
const passes = [];
|
|
3158
|
-
const commandsDir =
|
|
3159
|
-
const mdFiles =
|
|
4044
|
+
const commandsDir = resolve10(dir, "commands");
|
|
4045
|
+
const mdFiles = readdirSync9(commandsDir).filter((f) => f.endsWith(".md"));
|
|
3160
4046
|
if (mdFiles.length === 0) {
|
|
3161
4047
|
errors.push("commands/ directory has no .md files");
|
|
3162
4048
|
return { errors, warnings, passes };
|
|
3163
4049
|
}
|
|
3164
4050
|
passes.push(`${mdFiles.length} command definition(s) found`);
|
|
3165
4051
|
for (const file of mdFiles) {
|
|
3166
|
-
const filePath =
|
|
4052
|
+
const filePath = join15(commandsDir, file);
|
|
3167
4053
|
const raw = await Bun.file(filePath).text();
|
|
3168
4054
|
try {
|
|
3169
4055
|
const parsed = parseFrontmatter(raw);
|
|
@@ -3192,8 +4078,8 @@ var init_command = __esm(() => {
|
|
|
3192
4078
|
});
|
|
3193
4079
|
|
|
3194
4080
|
// src/validators/claude/memory.ts
|
|
3195
|
-
import { existsSync as
|
|
3196
|
-
import { resolve as
|
|
4081
|
+
import { existsSync as existsSync20 } from "fs";
|
|
4082
|
+
import { resolve as resolve11 } from "path";
|
|
3197
4083
|
var claudeMemoryValidator;
|
|
3198
4084
|
var init_memory = __esm(() => {
|
|
3199
4085
|
claudeMemoryValidator = {
|
|
@@ -3202,13 +4088,13 @@ var init_memory = __esm(() => {
|
|
|
3202
4088
|
name: "Claude CLAUDE.md",
|
|
3203
4089
|
description: "Validates CLAUDE.md: non-empty, length recommendations, @path imports",
|
|
3204
4090
|
detect(dir) {
|
|
3205
|
-
return
|
|
4091
|
+
return existsSync20(resolve11(dir, "CLAUDE.md"));
|
|
3206
4092
|
},
|
|
3207
4093
|
async validate(dir, _opts) {
|
|
3208
4094
|
const errors = [];
|
|
3209
4095
|
const warnings = [];
|
|
3210
4096
|
const passes = [];
|
|
3211
|
-
const filePath =
|
|
4097
|
+
const filePath = resolve11(dir, "CLAUDE.md");
|
|
3212
4098
|
const raw = await Bun.file(filePath).text();
|
|
3213
4099
|
if (!raw.trim()) {
|
|
3214
4100
|
errors.push("CLAUDE.md is empty");
|
|
@@ -3226,8 +4112,8 @@ var init_memory = __esm(() => {
|
|
|
3226
4112
|
let match;
|
|
3227
4113
|
while ((match = importRegex.exec(raw)) !== null) {
|
|
3228
4114
|
const importPath = match[1];
|
|
3229
|
-
const resolvedImport =
|
|
3230
|
-
if (
|
|
4115
|
+
const resolvedImport = resolve11(dir, importPath);
|
|
4116
|
+
if (existsSync20(resolvedImport)) {
|
|
3231
4117
|
passes.push(`@import "${importPath}" exists`);
|
|
3232
4118
|
} else {
|
|
3233
4119
|
warnings.push(`@import "${importPath}" \u2014 file not found at ${resolvedImport}`);
|
|
@@ -3238,6 +4124,165 @@ var init_memory = __esm(() => {
|
|
|
3238
4124
|
};
|
|
3239
4125
|
});
|
|
3240
4126
|
|
|
4127
|
+
// src/validators/claude/lsp.ts
|
|
4128
|
+
import { existsSync as existsSync21 } from "fs";
|
|
4129
|
+
import { resolve as resolve12 } from "path";
|
|
4130
|
+
var claudeLspValidator;
|
|
4131
|
+
var init_lsp = __esm(() => {
|
|
4132
|
+
claudeLspValidator = {
|
|
4133
|
+
id: "claude:lsp",
|
|
4134
|
+
provider: "claude",
|
|
4135
|
+
name: "Claude LSP Servers",
|
|
4136
|
+
description: "Validates .lsp.json (or plugin.json lspServers): language server configs with required command + extensionToLanguage; optional transport, env, settings, diagnostics etc. (binaries installed separately)",
|
|
4137
|
+
detect(dir) {
|
|
4138
|
+
return existsSync21(resolve12(dir, ".lsp.json")) || existsSync21(resolve12(dir, ".claude-plugin", "plugin.json"));
|
|
4139
|
+
},
|
|
4140
|
+
async validate(dir, _opts) {
|
|
4141
|
+
const errors = [];
|
|
4142
|
+
const warnings = [];
|
|
4143
|
+
const passes = [];
|
|
4144
|
+
let cfg = null;
|
|
4145
|
+
const lspPath = resolve12(dir, ".lsp.json");
|
|
4146
|
+
if (existsSync21(lspPath)) {
|
|
4147
|
+
try {
|
|
4148
|
+
cfg = JSON.parse(await Bun.file(lspPath).text());
|
|
4149
|
+
passes.push(".lsp.json is valid JSON");
|
|
4150
|
+
} catch {
|
|
4151
|
+
errors.push(".lsp.json is invalid JSON");
|
|
4152
|
+
return { errors, warnings, passes };
|
|
4153
|
+
}
|
|
4154
|
+
} else {
|
|
4155
|
+
const manifestPath = resolve12(dir, ".claude-plugin", "plugin.json");
|
|
4156
|
+
if (existsSync21(manifestPath)) {
|
|
4157
|
+
try {
|
|
4158
|
+
const m = JSON.parse(await Bun.file(manifestPath).text());
|
|
4159
|
+
if (m && m.lspServers && typeof m.lspServers === "object") {
|
|
4160
|
+
cfg = m.lspServers;
|
|
4161
|
+
passes.push("lspServers present inline in plugin.json");
|
|
4162
|
+
}
|
|
4163
|
+
} catch {}
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
if (!cfg) {
|
|
4167
|
+
if (!existsSync21(lspPath)) {
|
|
4168
|
+
return { errors, warnings, passes };
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
if (cfg && typeof cfg === "object") {
|
|
4172
|
+
const langs = Object.keys(cfg);
|
|
4173
|
+
passes.push(`${langs.length} language server(s) configured`);
|
|
4174
|
+
for (const lang of langs) {
|
|
4175
|
+
const entry = cfg[lang];
|
|
4176
|
+
if (!entry || !entry.command) {
|
|
4177
|
+
errors.push(`lsp "${lang}": "command" (the LSP binary) is required`);
|
|
4178
|
+
}
|
|
4179
|
+
if (!entry.extensionToLanguage || typeof entry.extensionToLanguage !== "object") {
|
|
4180
|
+
errors.push(`lsp "${lang}": "extensionToLanguage" map is required (e.g. { ".ts": "typescript" })`);
|
|
4181
|
+
} else {
|
|
4182
|
+
passes.push(`lsp "${lang}": has extensionToLanguage mapping`);
|
|
4183
|
+
}
|
|
4184
|
+
if (entry.diagnostics === false) {
|
|
4185
|
+
passes.push(`lsp "${lang}": diagnostics disabled (navigation only)`);
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
warnings.push('Reminder: the actual language server binary (gopls, pyright, etc.) must be installed separately on PATH. See /plugin errors tab if "Executable not found".');
|
|
4190
|
+
return { errors, warnings, passes };
|
|
4191
|
+
}
|
|
4192
|
+
};
|
|
4193
|
+
});
|
|
4194
|
+
|
|
4195
|
+
// src/validators/claude/monitors.ts
|
|
4196
|
+
import { existsSync as existsSync22 } from "fs";
|
|
4197
|
+
import { resolve as resolve13 } from "path";
|
|
4198
|
+
var claudeMonitorsValidator;
|
|
4199
|
+
var init_monitors = __esm(() => {
|
|
4200
|
+
claudeMonitorsValidator = {
|
|
4201
|
+
id: "claude:monitors",
|
|
4202
|
+
provider: "claude",
|
|
4203
|
+
name: "Claude Monitors (experimental)",
|
|
4204
|
+
description: "Validates monitors/monitors.json (or experimental.monitors): array of {name, command, description, when?}; commands support ${CLAUDE_PLUGIN_*} subs. Monitors run only in interactive CLI sessions.",
|
|
4205
|
+
detect(dir) {
|
|
4206
|
+
return existsSync22(resolve13(dir, "monitors", "monitors.json")) || existsSync22(resolve13(dir, "monitors.json")) || existsSync22(resolve13(dir, ".claude-plugin", "plugin.json"));
|
|
4207
|
+
},
|
|
4208
|
+
async validate(dir, _opts) {
|
|
4209
|
+
const errors = [];
|
|
4210
|
+
const warnings = [];
|
|
4211
|
+
const passes = [];
|
|
4212
|
+
let arr = null;
|
|
4213
|
+
const candidates = [
|
|
4214
|
+
resolve13(dir, "monitors", "monitors.json"),
|
|
4215
|
+
resolve13(dir, "monitors.json")
|
|
4216
|
+
];
|
|
4217
|
+
for (const p of candidates) {
|
|
4218
|
+
if (existsSync22(p)) {
|
|
4219
|
+
try {
|
|
4220
|
+
const parsed = JSON.parse(await Bun.file(p).text());
|
|
4221
|
+
if (Array.isArray(parsed)) {
|
|
4222
|
+
arr = parsed;
|
|
4223
|
+
passes.push("monitors config is valid JSON array");
|
|
4224
|
+
}
|
|
4225
|
+
break;
|
|
4226
|
+
} catch {
|
|
4227
|
+
errors.push("monitors config is invalid JSON");
|
|
4228
|
+
return { errors, warnings, passes };
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
if (!arr) {
|
|
4233
|
+
const mp = resolve13(dir, ".claude-plugin", "plugin.json");
|
|
4234
|
+
if (existsSync22(mp)) {
|
|
4235
|
+
try {
|
|
4236
|
+
const m = JSON.parse(await Bun.file(mp).text());
|
|
4237
|
+
const exp = m?.experimental;
|
|
4238
|
+
const inline = typeof exp === "string" ? null : exp?.monitors;
|
|
4239
|
+
if (Array.isArray(inline))
|
|
4240
|
+
arr = inline;
|
|
4241
|
+
else if (typeof inline === "string") {
|
|
4242
|
+
passes.push("experimental.monitors declared as path in manifest (content not validated here)");
|
|
4243
|
+
}
|
|
4244
|
+
} catch {}
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
if (!arr) {
|
|
4248
|
+
return { errors, warnings, passes };
|
|
4249
|
+
}
|
|
4250
|
+
if (!Array.isArray(arr)) {
|
|
4251
|
+
errors.push("monitors config must be a JSON array");
|
|
4252
|
+
return { errors, warnings, passes };
|
|
4253
|
+
}
|
|
4254
|
+
const seen = new Set;
|
|
4255
|
+
arr.forEach((mon, i) => {
|
|
4256
|
+
if (!mon || typeof mon !== "object") {
|
|
4257
|
+
errors.push(`monitors[${i}]: entry must be an object`);
|
|
4258
|
+
return;
|
|
4259
|
+
}
|
|
4260
|
+
if (!mon.name || typeof mon.name !== "string") {
|
|
4261
|
+
errors.push(`monitors[${i}]: "name" (unique id) is required`);
|
|
4262
|
+
} else {
|
|
4263
|
+
if (seen.has(mon.name))
|
|
4264
|
+
errors.push(`monitors: duplicate name "${mon.name}"`);
|
|
4265
|
+
seen.add(mon.name);
|
|
4266
|
+
}
|
|
4267
|
+
if (!mon.command || typeof mon.command !== "string") {
|
|
4268
|
+
errors.push(`monitors[${i}]: "command" (shell command) is required`);
|
|
4269
|
+
} else if (/\$\{CLAUDE_/.test(mon.command)) {
|
|
4270
|
+
passes.push(`monitors[${i}] "${mon.name || i}": uses CLAUDE_PLUGIN_* substitution`);
|
|
4271
|
+
}
|
|
4272
|
+
if (!mon.description) {
|
|
4273
|
+
warnings.push(`monitors[${i}]: "description" recommended (shown in task panel)`);
|
|
4274
|
+
}
|
|
4275
|
+
if (mon.when && !/^always$|^on-skill-invoke:/.test(String(mon.when))) {
|
|
4276
|
+
warnings.push(`monitors[${i}]: "when" should be "always" (default) or "on-skill-invoke:<skill>"`);
|
|
4277
|
+
}
|
|
4278
|
+
});
|
|
4279
|
+
passes.push(`${arr.length} monitor(s) declared`);
|
|
4280
|
+
warnings.push("Note: monitors are experimental, run only for interactive CLI sessions, and are skipped on some hosts. They do not stop automatically if the plugin is disabled mid-session.");
|
|
4281
|
+
return { errors, warnings, passes };
|
|
4282
|
+
}
|
|
4283
|
+
};
|
|
4284
|
+
});
|
|
4285
|
+
|
|
3241
4286
|
// src/validators/index.ts
|
|
3242
4287
|
function resolveFor(forFlag, allValidators = validators) {
|
|
3243
4288
|
if (!forFlag) {
|
|
@@ -3272,6 +4317,8 @@ var init_validators = __esm(() => {
|
|
|
3272
4317
|
init_subagent();
|
|
3273
4318
|
init_command();
|
|
3274
4319
|
init_memory();
|
|
4320
|
+
init_lsp();
|
|
4321
|
+
init_monitors();
|
|
3275
4322
|
validators = [
|
|
3276
4323
|
claudeSkillValidator,
|
|
3277
4324
|
claudePluginValidator,
|
|
@@ -3280,14 +4327,16 @@ var init_validators = __esm(() => {
|
|
|
3280
4327
|
claudeMcpValidator,
|
|
3281
4328
|
claudeSubagentValidator,
|
|
3282
4329
|
claudeCommandValidator,
|
|
3283
|
-
claudeMemoryValidator
|
|
4330
|
+
claudeMemoryValidator,
|
|
4331
|
+
claudeLspValidator,
|
|
4332
|
+
claudeMonitorsValidator
|
|
3284
4333
|
];
|
|
3285
4334
|
});
|
|
3286
4335
|
|
|
3287
4336
|
// src/core/remote.ts
|
|
3288
4337
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
3289
4338
|
import { mkdtempSync, rmSync } from "fs";
|
|
3290
|
-
import { join as
|
|
4339
|
+
import { join as join16 } from "path";
|
|
3291
4340
|
import { tmpdir } from "os";
|
|
3292
4341
|
function parseRemoteUrl(input) {
|
|
3293
4342
|
if (input.startsWith(".") || input.startsWith("/") || input.startsWith("~")) {
|
|
@@ -3324,7 +4373,7 @@ function isGhAvailable() {
|
|
|
3324
4373
|
return ghAvailable;
|
|
3325
4374
|
}
|
|
3326
4375
|
async function cloneToTemp(parsed) {
|
|
3327
|
-
const tmpDir = mkdtempSync(
|
|
4376
|
+
const tmpDir = mkdtempSync(join16(tmpdir(), "dora-"));
|
|
3328
4377
|
const cleanup = () => {
|
|
3329
4378
|
try {
|
|
3330
4379
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
@@ -3372,15 +4421,15 @@ var exports_validate_top = {};
|
|
|
3372
4421
|
__export(exports_validate_top, {
|
|
3373
4422
|
default: () => validate_top_default
|
|
3374
4423
|
});
|
|
3375
|
-
import { existsSync as
|
|
3376
|
-
import { resolve as
|
|
3377
|
-
var
|
|
4424
|
+
import { existsSync as existsSync24 } from "fs";
|
|
4425
|
+
import { resolve as resolve14 } from "path";
|
|
4426
|
+
var import_picocolors13, validate_top_default;
|
|
3378
4427
|
var init_validate_top = __esm(() => {
|
|
3379
4428
|
init_dist();
|
|
3380
4429
|
init_out();
|
|
3381
4430
|
init_validators();
|
|
3382
4431
|
init_remote();
|
|
3383
|
-
|
|
4432
|
+
import_picocolors13 = __toESM(require_picocolors(), 1);
|
|
3384
4433
|
validate_top_default = defineCommand({
|
|
3385
4434
|
meta: {
|
|
3386
4435
|
name: "validate",
|
|
@@ -3420,24 +4469,24 @@ var init_validate_top = __esm(() => {
|
|
|
3420
4469
|
let cleanup;
|
|
3421
4470
|
if (remote) {
|
|
3422
4471
|
ui.info(`
|
|
3423
|
-
Cloning ${
|
|
4472
|
+
Cloning ${import_picocolors13.default.dim(args.path)}...`);
|
|
3424
4473
|
try {
|
|
3425
4474
|
const result = await cloneToTemp(remote);
|
|
3426
|
-
fullPath = remote.subpath ?
|
|
4475
|
+
fullPath = remote.subpath ? resolve14(result.dir, remote.subpath) : result.dir;
|
|
3427
4476
|
cleanup = result.cleanup;
|
|
3428
4477
|
} catch (err) {
|
|
3429
4478
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3430
4479
|
ui.fail(msg);
|
|
3431
4480
|
process.exit(1);
|
|
3432
4481
|
}
|
|
3433
|
-
if (!
|
|
4482
|
+
if (!existsSync24(fullPath)) {
|
|
3434
4483
|
cleanup();
|
|
3435
4484
|
ui.fail(`Subdirectory not found in repo: ${remote.subpath}`);
|
|
3436
4485
|
process.exit(1);
|
|
3437
4486
|
}
|
|
3438
4487
|
} else {
|
|
3439
|
-
fullPath =
|
|
3440
|
-
if (!
|
|
4488
|
+
fullPath = resolve14(args.path);
|
|
4489
|
+
if (!existsSync24(fullPath)) {
|
|
3441
4490
|
ui.fail(`Path not found: ${args.path}
|
|
3442
4491
|
|
|
3443
4492
|
Check that the path is correct and the directory exists.`);
|
|
@@ -3468,13 +4517,13 @@ Check that the path is correct and the directory exists.`);
|
|
|
3468
4517
|
` + `Available providers:
|
|
3469
4518
|
` + providers.map((p) => {
|
|
3470
4519
|
const pvs = validators.filter((v) => v.provider === p);
|
|
3471
|
-
return ` ${
|
|
3472
|
-
` + pvs.map((v) => ` \u2022 ${
|
|
4520
|
+
return ` ${import_picocolors13.default.bold(p)}
|
|
4521
|
+
` + pvs.map((v) => ` \u2022 ${import_picocolors13.default.dim(v.id)} \u2014 ${v.description}`).join(`
|
|
3473
4522
|
`);
|
|
3474
4523
|
}).join(`
|
|
3475
4524
|
`) + `
|
|
3476
4525
|
|
|
3477
|
-
Use ${
|
|
4526
|
+
Use ${import_picocolors13.default.dim("--for <provider>")} or ${import_picocolors13.default.dim("--for <provider:type>")} to target explicitly.`);
|
|
3478
4527
|
process.exit(1);
|
|
3479
4528
|
}
|
|
3480
4529
|
const allResults = [];
|
|
@@ -3495,7 +4544,7 @@ Use ${import_picocolors11.default.dim("--for <provider>")} or ${import_picocolor
|
|
|
3495
4544
|
} else {
|
|
3496
4545
|
for (const { id, name, result } of allResults) {
|
|
3497
4546
|
ui.write(`
|
|
3498
|
-
${
|
|
4547
|
+
${import_picocolors13.default.bold("dora validate")} \u2014 ${import_picocolors13.default.white(name)} ${import_picocolors13.default.dim(`(${id})`)}
|
|
3499
4548
|
`);
|
|
3500
4549
|
ui.info(` Path: ${args.path}
|
|
3501
4550
|
`);
|
|
@@ -3510,7 +4559,7 @@ Use ${import_picocolors11.default.dim("--for <provider>")} or ${import_picocolor
|
|
|
3510
4559
|
}
|
|
3511
4560
|
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
3512
4561
|
ui.write(`
|
|
3513
|
-
${
|
|
4562
|
+
${import_picocolors13.default.green("\u2713")} ${import_picocolors13.default.white("All checks passed.")}
|
|
3514
4563
|
`);
|
|
3515
4564
|
} else {
|
|
3516
4565
|
ui.info(`
|
|
@@ -3532,16 +4581,16 @@ var exports_init2 = {};
|
|
|
3532
4581
|
__export(exports_init2, {
|
|
3533
4582
|
default: () => init_default2
|
|
3534
4583
|
});
|
|
3535
|
-
import { basename as
|
|
4584
|
+
import { basename as basename4, join as join17 } from "path";
|
|
3536
4585
|
var {spawnSync: spawnSync5 } = globalThis.Bun;
|
|
3537
|
-
var
|
|
4586
|
+
var import_picocolors14, init_default2;
|
|
3538
4587
|
var init_init2 = __esm(() => {
|
|
3539
4588
|
init_dist();
|
|
3540
4589
|
init_out();
|
|
3541
4590
|
init_journal_config();
|
|
3542
4591
|
init_journal_remote();
|
|
3543
4592
|
init_prompt();
|
|
3544
|
-
|
|
4593
|
+
import_picocolors14 = __toESM(require_picocolors(), 1);
|
|
3545
4594
|
init_default2 = defineCommand({
|
|
3546
4595
|
meta: {
|
|
3547
4596
|
name: "init",
|
|
@@ -3568,17 +4617,17 @@ var init_init2 = __esm(() => {
|
|
|
3568
4617
|
ui.heading("dora init \u2014 Set up doraval, your journal, and the coding agent dora should use on the fly");
|
|
3569
4618
|
const ghCheck = ensureGhCli();
|
|
3570
4619
|
if (!ghCheck.ok) {
|
|
3571
|
-
ui.write(` ${
|
|
4620
|
+
ui.write(` ${import_picocolors14.default.red("\u2717")} ${import_picocolors14.default.white("The GitHub CLI (")}${import_picocolors14.default.bold("gh")}${import_picocolors14.default.white(") is not installed.")}
|
|
3572
4621
|
`);
|
|
3573
|
-
ui.info(` doraval uses ${
|
|
4622
|
+
ui.info(` doraval uses ${import_picocolors14.default.bold("gh")} to fetch and sync journal files with GitHub.
|
|
3574
4623
|
`);
|
|
3575
4624
|
ui.info(` Install it:
|
|
3576
4625
|
`);
|
|
3577
|
-
ui.info(` macOS: ${
|
|
3578
|
-
ui.info(` Linux: ${
|
|
3579
|
-
ui.info(` Windows: ${
|
|
4626
|
+
ui.info(` macOS: ${import_picocolors14.default.dim("brew install gh")}`);
|
|
4627
|
+
ui.info(` Linux: ${import_picocolors14.default.dim("https://github.com/cli/cli/blob/trunk/docs/install_linux.md")}`);
|
|
4628
|
+
ui.info(` Windows: ${import_picocolors14.default.dim("winget install --id GitHub.cli")}
|
|
3580
4629
|
`);
|
|
3581
|
-
ui.info(` Then authenticate: ${
|
|
4630
|
+
ui.info(` Then authenticate: ${import_picocolors14.default.dim("gh auth login")}
|
|
3582
4631
|
`);
|
|
3583
4632
|
process.exit(1);
|
|
3584
4633
|
}
|
|
@@ -3591,44 +4640,44 @@ var init_init2 = __esm(() => {
|
|
|
3591
4640
|
if (gitOwner) {
|
|
3592
4641
|
defaultRepo = `${gitOwner}/${gitOwner}.md`;
|
|
3593
4642
|
if (ghLogin && ghLogin !== gitOwner) {
|
|
3594
|
-
sourceNote = ` ${
|
|
4643
|
+
sourceNote = ` ${import_picocolors14.default.dim("(from git remote; your active gh account is " + ghLogin + ")")}
|
|
3595
4644
|
`;
|
|
3596
4645
|
} else {
|
|
3597
|
-
sourceNote = ` ${
|
|
4646
|
+
sourceNote = ` ${import_picocolors14.default.dim("(from git remote)")}
|
|
3598
4647
|
`;
|
|
3599
4648
|
}
|
|
3600
4649
|
} else if (ghLogin) {
|
|
3601
4650
|
defaultRepo = `${ghLogin}/${ghLogin}.md`;
|
|
3602
|
-
sourceNote = ` ${
|
|
4651
|
+
sourceNote = ` ${import_picocolors14.default.dim("(from your active gh account)")}
|
|
3603
4652
|
`;
|
|
3604
4653
|
} else {
|
|
3605
|
-
ui.warn(`Not logged in to GitHub. Run ${
|
|
4654
|
+
ui.warn(`Not logged in to GitHub. Run ${import_picocolors14.default.dim("gh auth login")} first.
|
|
3606
4655
|
`);
|
|
3607
4656
|
process.exit(1);
|
|
3608
4657
|
}
|
|
3609
4658
|
const existingConfig = await readConfig();
|
|
3610
4659
|
if (existingConfig?.journal.repo) {
|
|
3611
4660
|
defaultRepo = existingConfig.journal.repo;
|
|
3612
|
-
sourceNote = ` ${
|
|
4661
|
+
sourceNote = ` ${import_picocolors14.default.dim("(from your previous journal setup)")}
|
|
3613
4662
|
`;
|
|
3614
4663
|
}
|
|
3615
|
-
ui.info(` Journal repo ${
|
|
4664
|
+
ui.info(` Journal repo ${import_picocolors14.default.dim("(owner/name)")}`);
|
|
3616
4665
|
if (sourceNote)
|
|
3617
4666
|
ui.write(sourceNote);
|
|
3618
4667
|
repo = prompt(" >", defaultRepo);
|
|
3619
4668
|
}
|
|
3620
4669
|
let project = args.project || process.env.DORAVAL_PROJECT;
|
|
3621
4670
|
if (!project) {
|
|
3622
|
-
const defaultProject =
|
|
4671
|
+
const defaultProject = basename4(process.cwd());
|
|
3623
4672
|
project = prompt(" Project name", defaultProject);
|
|
3624
4673
|
}
|
|
3625
4674
|
project = sanitizeProjectName(project);
|
|
3626
4675
|
if (!repoExists(repo)) {
|
|
3627
|
-
ui.write(` ${
|
|
4676
|
+
ui.write(` ${import_picocolors14.default.red("\u2717")} ${import_picocolors14.default.white("Repository")} ${import_picocolors14.default.bold(repo)} ${import_picocolors14.default.white("not found on GitHub.")}
|
|
3628
4677
|
`);
|
|
3629
4678
|
ui.info(` Create it first:
|
|
3630
4679
|
`);
|
|
3631
|
-
ui.info(` ${
|
|
4680
|
+
ui.info(` ${import_picocolors14.default.dim(`gh repo create ${repo} --private --description "Personal journal for agent decisions"`)}
|
|
3632
4681
|
`);
|
|
3633
4682
|
process.exit(1);
|
|
3634
4683
|
}
|
|
@@ -3636,16 +4685,16 @@ var init_init2 = __esm(() => {
|
|
|
3636
4685
|
const alreadyRegistered = existing?.journal.projects[project];
|
|
3637
4686
|
const isRefresh = alreadyRegistered && args.refresh;
|
|
3638
4687
|
if (alreadyRegistered && !isRefresh) {
|
|
3639
|
-
ui.write(` ${
|
|
4688
|
+
ui.write(` ${import_picocolors14.default.yellow("\u26A0")} ${import_picocolors14.default.white("Project")} ${import_picocolors14.default.bold(project)} ${import_picocolors14.default.white("is already registered.")}
|
|
3640
4689
|
`);
|
|
3641
4690
|
ui.info(` Repo: ${existing.journal.repo}
|
|
3642
4691
|
`);
|
|
3643
|
-
ui.info(` To refresh journal files, use ${
|
|
4692
|
+
ui.info(` To refresh journal files, use ${import_picocolors14.default.dim("dora journal update")} (or ${import_picocolors14.default.dim("dora init --refresh")}).
|
|
3644
4693
|
`);
|
|
3645
4694
|
}
|
|
3646
4695
|
const journalsDir = getJournalsDir();
|
|
3647
4696
|
const remotePath = `projects/${project}.md`;
|
|
3648
|
-
const localPath =
|
|
4697
|
+
const localPath = join17(journalsDir, `${project}.md`);
|
|
3649
4698
|
const effectiveRepo = isRefresh && !args.repo ? existing.journal.repo : repo;
|
|
3650
4699
|
const config = existing ?? {
|
|
3651
4700
|
journal: { repo: effectiveRepo, projects: {} }
|
|
@@ -3656,9 +4705,9 @@ var init_init2 = __esm(() => {
|
|
|
3656
4705
|
local_path: localPath
|
|
3657
4706
|
};
|
|
3658
4707
|
ensureDoravalDirs();
|
|
3659
|
-
ui.write(` ${
|
|
4708
|
+
ui.write(` ${import_picocolors14.default.dim(import_picocolors14.default.gray("Fetching journal files from"))} ${import_picocolors14.default.gray(effectiveRepo)}${import_picocolors14.default.dim(import_picocolors14.default.gray("..."))}
|
|
3660
4709
|
`);
|
|
3661
|
-
const globalDest =
|
|
4710
|
+
const globalDest = join17(journalsDir, "global.md");
|
|
3662
4711
|
const refreshGlobalRes = await refreshLocalJournalFile(effectiveRepo, "global.md", globalDest);
|
|
3663
4712
|
let wroteGlobal;
|
|
3664
4713
|
if (!refreshGlobalRes.ok) {
|
|
@@ -3675,7 +4724,7 @@ var init_init2 = __esm(() => {
|
|
|
3675
4724
|
if (wroteGlobal) {
|
|
3676
4725
|
ui.success("global.md");
|
|
3677
4726
|
} else {
|
|
3678
|
-
ui.write(` ${
|
|
4727
|
+
ui.write(` ${import_picocolors14.default.dim("\xB7")} global.md ${import_picocolors14.default.dim("(not found \u2014 will be created on first sync)")}`);
|
|
3679
4728
|
await Bun.write(globalDest, `# Global Journal
|
|
3680
4729
|
|
|
3681
4730
|
Cross-project principles.
|
|
@@ -3697,7 +4746,7 @@ Cross-project principles.
|
|
|
3697
4746
|
if (wroteProject) {
|
|
3698
4747
|
ui.success(remotePath);
|
|
3699
4748
|
} else {
|
|
3700
|
-
ui.write(` ${
|
|
4749
|
+
ui.write(` ${import_picocolors14.default.dim("\xB7")} ${remotePath} ${import_picocolors14.default.dim("(not found \u2014 will be created on first sync)")}`);
|
|
3701
4750
|
await Bun.write(localPath, `# ${project} Journal
|
|
3702
4751
|
|
|
3703
4752
|
Project-specific decisions.
|
|
@@ -3705,13 +4754,13 @@ Project-specific decisions.
|
|
|
3705
4754
|
}
|
|
3706
4755
|
await writeConfig(config);
|
|
3707
4756
|
ui.write(`
|
|
3708
|
-
${
|
|
4757
|
+
${import_picocolors14.default.green("\u2713")} ${import_picocolors14.default.white("Journal ready for project")} ${import_picocolors14.default.bold(import_picocolors14.default.white(project))}.
|
|
3709
4758
|
`);
|
|
3710
4759
|
const existingAgent = (await readConfig())?.agent;
|
|
3711
4760
|
if (existingAgent?.command) {
|
|
3712
|
-
ui.write(` ${
|
|
4761
|
+
ui.write(` ${import_picocolors14.default.bold(import_picocolors14.default.white("Coding agent (already configured)"))}
|
|
3713
4762
|
`);
|
|
3714
|
-
ui.write(` Current: ${
|
|
4763
|
+
ui.write(` Current: ${import_picocolors14.default.dim(import_picocolors14.default.gray(existingAgent.command))} template: ${import_picocolors14.default.dim(import_picocolors14.default.gray(existingAgent.prompt_template || "(default)"))}
|
|
3715
4764
|
`);
|
|
3716
4765
|
const change = prompt(" Reconfigure / change the coding agent for on-the-fly enrichment? (y/N)", "n");
|
|
3717
4766
|
if (!/^y/i.test(String(change))) {
|
|
@@ -3721,16 +4770,16 @@ Project-specific decisions.
|
|
|
3721
4770
|
if (existingAgent)
|
|
3722
4771
|
cfg.agent = existingAgent;
|
|
3723
4772
|
await writeConfig(cfg);
|
|
3724
|
-
ui.write(` ${
|
|
4773
|
+
ui.write(` ${import_picocolors14.default.green("\u2713")} ${import_picocolors14.default.white("Try:")} ${import_picocolors14.default.dim(import_picocolors14.default.gray('dora journal add "short decision"'))}
|
|
3725
4774
|
`);
|
|
3726
4775
|
process.exit(0);
|
|
3727
4776
|
return;
|
|
3728
4777
|
}
|
|
3729
4778
|
ui.blank();
|
|
3730
4779
|
} else {
|
|
3731
|
-
ui.write(` ${
|
|
4780
|
+
ui.write(` ${import_picocolors14.default.bold(import_picocolors14.default.white("Coding agent for journal add"))}
|
|
3732
4781
|
`);
|
|
3733
|
-
ui.info(` When configured, ${
|
|
4782
|
+
ui.info(` When configured, ${import_picocolors14.default.dim(import_picocolors14.default.gray('dora journal add ".."'))} will use your agent to enrich entries with tags and rationale automatically.
|
|
3734
4783
|
`);
|
|
3735
4784
|
}
|
|
3736
4785
|
const common = [
|
|
@@ -3749,7 +4798,7 @@ Project-specific decisions.
|
|
|
3749
4798
|
}
|
|
3750
4799
|
}
|
|
3751
4800
|
let agentCmd = detected || "claude";
|
|
3752
|
-
ui.write(` Detected / default agent command: ${
|
|
4801
|
+
ui.write(` Detected / default agent command: ${import_picocolors14.default.dim(import_picocolors14.default.gray(agentCmd))}`);
|
|
3753
4802
|
agentCmd = prompt(" Agent command (the binary you run for prompts)", agentCmd);
|
|
3754
4803
|
let template = detected ? common.find((c) => c.name === detected)?.template || '-p "{{prompt}}" --output-format json' : '-p "{{prompt}}" --output-format json';
|
|
3755
4804
|
ui.info(` Prompt template (use {{prompt}} placeholder):`);
|
|
@@ -3761,84 +4810,255 @@ Project-specific decisions.
|
|
|
3761
4810
|
};
|
|
3762
4811
|
await writeConfig(finalConfig);
|
|
3763
4812
|
ui.write(`
|
|
3764
|
-
${
|
|
4813
|
+
${import_picocolors14.default.green("\u2713")} ${import_picocolors14.default.white("Agent configured.")}
|
|
3765
4814
|
`);
|
|
3766
|
-
ui.info(` Re-run ${
|
|
4815
|
+
ui.info(` Re-run ${import_picocolors14.default.dim(import_picocolors14.default.gray("dora init"))} anytime to change it.
|
|
3767
4816
|
`);
|
|
3768
|
-
ui.info(` Next: ${
|
|
4817
|
+
ui.info(` Next: ${import_picocolors14.default.dim(import_picocolors14.default.gray('dora journal add ".."'))}, ${import_picocolors14.default.dim(import_picocolors14.default.gray("dora journal list"))}, or ${import_picocolors14.default.dim(import_picocolors14.default.gray("dora journal update"))}.
|
|
3769
4818
|
`);
|
|
3770
4819
|
process.exit(0);
|
|
3771
4820
|
}
|
|
3772
4821
|
});
|
|
3773
4822
|
});
|
|
3774
4823
|
|
|
3775
|
-
// src/
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
devDependencies: {
|
|
3787
|
-
"@types/bun": "latest"
|
|
3788
|
-
},
|
|
3789
|
-
bin: {
|
|
3790
|
-
doraval: "bin/doraval-wrapper.js",
|
|
3791
|
-
dora: "bin/doraval-wrapper.js"
|
|
3792
|
-
},
|
|
3793
|
-
description: "The context engineering toolkit for coding agents",
|
|
3794
|
-
engines: {
|
|
3795
|
-
bun: ">=1.2.0",
|
|
3796
|
-
node: ">=14.18.0"
|
|
3797
|
-
},
|
|
3798
|
-
files: [
|
|
3799
|
-
"bin/",
|
|
3800
|
-
"dist/",
|
|
3801
|
-
"README.md"
|
|
3802
|
-
],
|
|
3803
|
-
keywords: [
|
|
3804
|
-
"cli",
|
|
3805
|
-
"skills",
|
|
3806
|
-
"plugins",
|
|
3807
|
-
"agent",
|
|
3808
|
-
"validation",
|
|
3809
|
-
"lint",
|
|
3810
|
-
"claude-code",
|
|
3811
|
-
"grok",
|
|
3812
|
-
"cursor",
|
|
3813
|
-
"windsurf",
|
|
3814
|
-
"mcp"
|
|
3815
|
-
],
|
|
3816
|
-
license: "MIT",
|
|
3817
|
-
workspaces: [
|
|
3818
|
-
"apps/*"
|
|
3819
|
-
],
|
|
3820
|
-
scripts: {
|
|
3821
|
-
build: "bun build ./src/cli/index.ts --outfile ./bin/doraval.js --target bun",
|
|
3822
|
-
dev: "bun run ./src/cli/index.ts",
|
|
3823
|
-
test: "bun test",
|
|
3824
|
-
typecheck: "bunx tsc --noEmit --skipLibCheck",
|
|
3825
|
-
prepublishOnly: `bun run build && node -e "const p=require('./package.json'),j=require('./jsr.json');if(p.version!==j.version){console.error('Version mismatch: package.json='+p.version+' jsr.json='+j.version);process.exit(1)}"`,
|
|
3826
|
-
bump: "bun run scripts/bump.ts",
|
|
3827
|
-
release: "bun run scripts/release.ts",
|
|
3828
|
-
"jsr:publish": "bunx jsr publish",
|
|
3829
|
-
"site:dev": "cd apps/website && bun run dev",
|
|
3830
|
-
"site:build": "cd apps/website && bun run build",
|
|
3831
|
-
"site:preview": "cd apps/website && bun run preview"
|
|
3832
|
-
},
|
|
3833
|
-
type: "module",
|
|
3834
|
-
dependencies: {
|
|
3835
|
-
citty: "^0.2.2",
|
|
3836
|
-
picocolors: "^1.1.1"
|
|
4824
|
+
// src/core/update.ts
|
|
4825
|
+
import { execSync } from "child_process";
|
|
4826
|
+
import { existsSync as existsSync25 } from "fs";
|
|
4827
|
+
import { resolve as resolve15 } from "path";
|
|
4828
|
+
import { homedir as homedir2 } from "os";
|
|
4829
|
+
function isInPath(cmd) {
|
|
4830
|
+
try {
|
|
4831
|
+
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
4832
|
+
return true;
|
|
4833
|
+
} catch {
|
|
4834
|
+
return false;
|
|
3837
4835
|
}
|
|
3838
|
-
}
|
|
4836
|
+
}
|
|
4837
|
+
async function autoDetect() {
|
|
4838
|
+
const execPath = process.execPath;
|
|
4839
|
+
const argv0 = process.argv[0] || "";
|
|
4840
|
+
if (execPath.includes("/Cellar/") || execPath.includes("/homebrew/") || execPath.includes("/opt/homebrew/")) {
|
|
4841
|
+
if (isInPath("brew"))
|
|
4842
|
+
return { type: "homebrew" };
|
|
4843
|
+
}
|
|
4844
|
+
if (execPath.includes("/.npm/") || argv0.includes("npm")) {
|
|
4845
|
+
return { type: "npm" };
|
|
4846
|
+
}
|
|
4847
|
+
if (execPath.includes("/.bun/") || argv0.includes("bun")) {
|
|
4848
|
+
return { type: "bun" };
|
|
4849
|
+
}
|
|
4850
|
+
const home = homedir2();
|
|
4851
|
+
const possibleGlobals = [
|
|
4852
|
+
resolve15(home, ".npm-global/bin/doraval"),
|
|
4853
|
+
resolve15(home, ".bun/bin/doraval")
|
|
4854
|
+
];
|
|
4855
|
+
for (const p of possibleGlobals) {
|
|
4856
|
+
if (existsSync25(p)) {
|
|
4857
|
+
if (p.includes(".npm"))
|
|
4858
|
+
return { type: "npm" };
|
|
4859
|
+
if (p.includes(".bun"))
|
|
4860
|
+
return { type: "bun" };
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
return null;
|
|
4864
|
+
}
|
|
4865
|
+
async function detectInstallMethod(options) {
|
|
4866
|
+
if (options?.force) {
|
|
4867
|
+
if (["homebrew", "npm", "bun"].includes(options.force)) {
|
|
4868
|
+
return { type: options.force };
|
|
4869
|
+
}
|
|
4870
|
+
if (options.force === "npx" || options.force === "bunx") {
|
|
4871
|
+
return { type: "transient", via: options.force };
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
const auto = await autoDetect();
|
|
4875
|
+
if (auto)
|
|
4876
|
+
return auto;
|
|
4877
|
+
const marker = await readInstallMarker();
|
|
4878
|
+
if (marker)
|
|
4879
|
+
return marker;
|
|
4880
|
+
return { type: "transient", via: "npx" };
|
|
4881
|
+
}
|
|
4882
|
+
async function fetchLatestVersionInfo() {
|
|
4883
|
+
const npmRes = await fetch("https://registry.npmjs.org/@hacksmith/doraval/latest");
|
|
4884
|
+
if (!npmRes.ok)
|
|
4885
|
+
throw new Error("Failed to fetch from npm");
|
|
4886
|
+
const npmData = await npmRes.json();
|
|
4887
|
+
const version = npmData.version;
|
|
4888
|
+
let summary = "New release available.";
|
|
4889
|
+
try {
|
|
4890
|
+
const ghRes = await fetch("https://api.github.com/repos/saif-shines/doraval/releases/latest", {
|
|
4891
|
+
headers: { "User-Agent": "doraval-update" }
|
|
4892
|
+
});
|
|
4893
|
+
if (ghRes.ok) {
|
|
4894
|
+
const ghData = await ghRes.json();
|
|
4895
|
+
const body = (ghData.body || "").trim();
|
|
4896
|
+
const lines = body.split(`
|
|
4897
|
+
`).filter((l) => l.trim().startsWith("-") || l.trim().startsWith("*")).slice(0, 2);
|
|
4898
|
+
if (lines.length)
|
|
4899
|
+
summary = lines.join(" ").slice(0, 200);
|
|
4900
|
+
else if (body)
|
|
4901
|
+
summary = body.split(`
|
|
4902
|
+
`)[0].slice(0, 150);
|
|
4903
|
+
}
|
|
4904
|
+
} catch {}
|
|
4905
|
+
return { version, summary };
|
|
4906
|
+
}
|
|
4907
|
+
function buildUpgradeCommand(method) {
|
|
4908
|
+
switch (method.type) {
|
|
4909
|
+
case "homebrew":
|
|
4910
|
+
return ["brew", "upgrade", "doraval"];
|
|
4911
|
+
case "npm":
|
|
4912
|
+
return ["npm", "install", "-g", "@hacksmith/doraval@latest"];
|
|
4913
|
+
case "bun":
|
|
4914
|
+
return ["bun", "add", "-g", "@hacksmith/doraval@latest"];
|
|
4915
|
+
default:
|
|
4916
|
+
throw new Error("Cannot build upgrade command for transient installs");
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
function shouldUpdate(current, latest) {
|
|
4920
|
+
if (current === latest)
|
|
4921
|
+
return false;
|
|
4922
|
+
const c = current.split(".").map(Number);
|
|
4923
|
+
const l = latest.split(".").map(Number);
|
|
4924
|
+
for (let i = 0;i < 3; i++) {
|
|
4925
|
+
if ((l[i] || 0) > (c[i] || 0))
|
|
4926
|
+
return true;
|
|
4927
|
+
if ((l[i] || 0) < (c[i] || 0))
|
|
4928
|
+
return false;
|
|
4929
|
+
}
|
|
4930
|
+
return false;
|
|
4931
|
+
}
|
|
4932
|
+
async function readInstallMarker() {
|
|
4933
|
+
try {
|
|
4934
|
+
const { readFile } = await import("fs/promises");
|
|
4935
|
+
const data = await readFile(MARKER_PATH, "utf8");
|
|
4936
|
+
const parsed = JSON.parse(data);
|
|
4937
|
+
if (parsed && parsed.type)
|
|
4938
|
+
return parsed;
|
|
4939
|
+
} catch {}
|
|
4940
|
+
return null;
|
|
4941
|
+
}
|
|
4942
|
+
async function writeInstallMarker(method) {
|
|
4943
|
+
try {
|
|
4944
|
+
const { mkdir, writeFile } = await import("fs/promises");
|
|
4945
|
+
const { dirname: dirname2 } = await import("path");
|
|
4946
|
+
await mkdir(dirname2(MARKER_PATH), { recursive: true });
|
|
4947
|
+
await writeFile(MARKER_PATH, JSON.stringify(method, null, 2));
|
|
4948
|
+
} catch {}
|
|
4949
|
+
}
|
|
4950
|
+
var MARKER_PATH;
|
|
4951
|
+
var init_update2 = __esm(() => {
|
|
4952
|
+
MARKER_PATH = resolve15(homedir2(), ".doraval", "install.json");
|
|
4953
|
+
});
|
|
4954
|
+
|
|
4955
|
+
// src/cli/commands/update.ts
|
|
4956
|
+
var exports_update2 = {};
|
|
4957
|
+
__export(exports_update2, {
|
|
4958
|
+
default: () => update_default2
|
|
4959
|
+
});
|
|
4960
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
4961
|
+
async function confirmUpdate() {
|
|
4962
|
+
const { createInterface } = await import("readline");
|
|
4963
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
4964
|
+
return new Promise((resolve16) => {
|
|
4965
|
+
rl.question("Update now? (y/N) ", (answer) => {
|
|
4966
|
+
rl.close();
|
|
4967
|
+
resolve16(answer.toLowerCase().startsWith("y"));
|
|
4968
|
+
});
|
|
4969
|
+
});
|
|
4970
|
+
}
|
|
4971
|
+
var update_default2;
|
|
4972
|
+
var init_update3 = __esm(() => {
|
|
4973
|
+
init_dist();
|
|
4974
|
+
init_out();
|
|
4975
|
+
init_update2();
|
|
4976
|
+
update_default2 = defineCommand({
|
|
4977
|
+
meta: {
|
|
4978
|
+
name: "update",
|
|
4979
|
+
description: "Update doraval to the latest version"
|
|
4980
|
+
},
|
|
4981
|
+
args: {
|
|
4982
|
+
check: {
|
|
4983
|
+
type: "boolean",
|
|
4984
|
+
description: "Only check for updates, do not install",
|
|
4985
|
+
default: false
|
|
4986
|
+
},
|
|
4987
|
+
yes: {
|
|
4988
|
+
type: "boolean",
|
|
4989
|
+
description: "Skip confirmation prompt",
|
|
4990
|
+
default: false
|
|
4991
|
+
}
|
|
4992
|
+
},
|
|
4993
|
+
async run({ args }) {
|
|
4994
|
+
const currentVersion = require_package().version;
|
|
4995
|
+
const argv1 = process.argv[1] || "";
|
|
4996
|
+
const isNpx = process.env.npm_execpath?.includes("npx") || argv1.includes("/.npm/") || process.env.npm_lifecycle_script?.includes("npx");
|
|
4997
|
+
const isBunx = process.env.BUN_INSTALL || argv1.includes(".bun/bin/bunx") || argv1.includes("bunx");
|
|
4998
|
+
if (isNpx || isBunx) {
|
|
4999
|
+
ui.info("It looks like you're using doraval via npx or bunx.");
|
|
5000
|
+
ui.info("These always fetch the latest version on the next run.");
|
|
5001
|
+
ui.info("");
|
|
5002
|
+
ui.info("For easier updates, install globally:");
|
|
5003
|
+
ui.info(" brew install saif-shines/tap/doraval");
|
|
5004
|
+
ui.info(" npm install -g @hacksmith/doraval");
|
|
5005
|
+
ui.info(" bun add -g @hacksmith/doraval");
|
|
5006
|
+
process.exit(0);
|
|
5007
|
+
}
|
|
5008
|
+
const method = await detectInstallMethod();
|
|
5009
|
+
if (method.type === "transient") {
|
|
5010
|
+
ui.info("Transient usage detected. Install globally for update support.");
|
|
5011
|
+
process.exit(0);
|
|
5012
|
+
}
|
|
5013
|
+
const latestInfo = await fetchLatestVersionInfo();
|
|
5014
|
+
if (!shouldUpdate(currentVersion, latestInfo.version)) {
|
|
5015
|
+
ui.success(`doraval is up to date (${currentVersion}).`);
|
|
5016
|
+
process.exit(0);
|
|
5017
|
+
}
|
|
5018
|
+
if (args.check) {
|
|
5019
|
+
ui.info(`Update available: ${currentVersion} \u2192 ${latestInfo.version}`);
|
|
5020
|
+
process.exit(1);
|
|
5021
|
+
}
|
|
5022
|
+
ui.heading("doraval update");
|
|
5023
|
+
ui.info(` Current: ${currentVersion}`);
|
|
5024
|
+
ui.info(` Latest: ${latestInfo.version}
|
|
5025
|
+
`);
|
|
5026
|
+
ui.info(` ${latestInfo.summary}
|
|
5027
|
+
`);
|
|
5028
|
+
if (!args.yes) {
|
|
5029
|
+
const confirmed = await confirmUpdate();
|
|
5030
|
+
if (!confirmed) {
|
|
5031
|
+
ui.info("Update cancelled.");
|
|
5032
|
+
process.exit(0);
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
const cmd = buildUpgradeCommand(method);
|
|
5036
|
+
ui.info(`Running: ${cmd.join(" ")}
|
|
5037
|
+
`);
|
|
5038
|
+
const result = spawnSync6(cmd[0], cmd.slice(1), { stdio: "inherit" });
|
|
5039
|
+
if (result.status === 0) {
|
|
5040
|
+
ui.success(`Successfully updated to ${latestInfo.version}.`);
|
|
5041
|
+
ui.info("You may need to restart your shell to pick up the new version.");
|
|
5042
|
+
await writeInstallMarker(method);
|
|
5043
|
+
} else {
|
|
5044
|
+
ui.fail("Update failed.");
|
|
5045
|
+
ui.info("Common fixes:");
|
|
5046
|
+
if (cmd[0] === "brew")
|
|
5047
|
+
ui.info(" \u2022 Try: sudo brew upgrade doraval or ensure you are in the admin group");
|
|
5048
|
+
if (cmd[0] === "npm" || cmd[0] === "bun")
|
|
5049
|
+
ui.info(" \u2022 Try running with appropriate permissions or check network.");
|
|
5050
|
+
ui.info(`
|
|
5051
|
+
Raw output above.`);
|
|
5052
|
+
process.exit(result.status ?? 1);
|
|
5053
|
+
}
|
|
5054
|
+
}
|
|
5055
|
+
});
|
|
5056
|
+
});
|
|
3839
5057
|
|
|
3840
5058
|
// src/cli/index.ts
|
|
3841
|
-
|
|
5059
|
+
init_dist();
|
|
5060
|
+
var import__package = __toESM(require_package(), 1);
|
|
5061
|
+
var import_picocolors15 = __toESM(require_picocolors(), 1);
|
|
3842
5062
|
var skill = defineCommand({
|
|
3843
5063
|
meta: {
|
|
3844
5064
|
name: "skill",
|
|
@@ -3875,12 +5095,26 @@ var claude = defineCommand({
|
|
|
3875
5095
|
description: "Claude Code-specific commands (packaging, scaffolding, distribution)"
|
|
3876
5096
|
},
|
|
3877
5097
|
subCommands: {
|
|
3878
|
-
new: () => Promise.resolve().then(() => (init_new(), exports_new)).then((m) => m.default)
|
|
5098
|
+
new: () => Promise.resolve().then(() => (init_new(), exports_new)).then((m) => m.default),
|
|
5099
|
+
bump: () => Promise.resolve().then(() => (init_bump(), exports_bump)).then((m) => m.default)
|
|
3879
5100
|
},
|
|
3880
5101
|
run() {
|
|
3881
5102
|
showUsage(claude);
|
|
3882
5103
|
}
|
|
3883
5104
|
});
|
|
5105
|
+
var codex = defineCommand({
|
|
5106
|
+
meta: {
|
|
5107
|
+
name: "codex",
|
|
5108
|
+
description: "Codex (OpenAI)-specific commands (packaging, scaffolding, distribution)"
|
|
5109
|
+
},
|
|
5110
|
+
subCommands: {
|
|
5111
|
+
new: () => Promise.resolve().then(() => (init_new2(), exports_new2)).then((m) => m.default),
|
|
5112
|
+
bump: () => Promise.resolve().then(() => (init_bump(), exports_bump)).then((m) => m.default)
|
|
5113
|
+
},
|
|
5114
|
+
run() {
|
|
5115
|
+
showUsage(codex);
|
|
5116
|
+
}
|
|
5117
|
+
});
|
|
3884
5118
|
var doraemonArt = `
|
|
3885
5119
|
\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28E0\u28E4\u28F4\u28F6\u28F6\u28F6\u28F6\u28F6\u2836\u28F6\u28E4\u28E4\u28C0\u2800\u2800\u2800\u2800\u2800\u2800
|
|
3886
5120
|
\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28E4\u28FE\u28FF\u28FF\u28FF\u2801\u2800\u2880\u2808\u28BF\u2880\u28C0\u2800\u2839\u28FF\u28FF\u28FF\u28E6\u28C4\u2800\u2800\u2800
|
|
@@ -3896,19 +5130,22 @@ var doraemonArt = `
|
|
|
3896
5130
|
var main = defineCommand({
|
|
3897
5131
|
meta: {
|
|
3898
5132
|
name: "doraval",
|
|
3899
|
-
version:
|
|
5133
|
+
version: import__package.default.version,
|
|
3900
5134
|
description: "The context engineering toolkit for coding agents"
|
|
3901
5135
|
},
|
|
3902
5136
|
subCommands: {
|
|
3903
5137
|
validate: () => Promise.resolve().then(() => (init_validate_top(), exports_validate_top)).then((m) => m.default),
|
|
3904
5138
|
init: () => Promise.resolve().then(() => (init_init2(), exports_init2)).then((m) => m.default),
|
|
5139
|
+
bump: () => Promise.resolve().then(() => (init_bump(), exports_bump)).then((m) => m.default),
|
|
5140
|
+
update: () => Promise.resolve().then(() => (init_update3(), exports_update2)).then((m) => m.default),
|
|
3905
5141
|
skill: () => Promise.resolve(skill),
|
|
3906
5142
|
journal: () => Promise.resolve(journal),
|
|
3907
|
-
claude: () => Promise.resolve(claude)
|
|
5143
|
+
claude: () => Promise.resolve(claude),
|
|
5144
|
+
codex: () => Promise.resolve(codex)
|
|
3908
5145
|
},
|
|
3909
5146
|
run() {
|
|
3910
5147
|
console.log(`
|
|
3911
|
-
` +
|
|
5148
|
+
` + import_picocolors15.default.blue(doraemonArt) + `
|
|
3912
5149
|
`);
|
|
3913
5150
|
showUsage(main);
|
|
3914
5151
|
}
|