@ccpluginizer/ccpluginizer 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2795 -205
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -899,8 +899,523 @@ class R2 {
|
|
|
899
899
|
}
|
|
900
900
|
}
|
|
901
901
|
|
|
902
|
+
// ../../node_modules/.bun/@crustjs+style@0.1.0+7524df1edfed9f02/node_modules/@crustjs/style/dist/index.js
|
|
903
|
+
var rF = Object.defineProperty;
|
|
904
|
+
var sF = (F3) => F3;
|
|
905
|
+
function aF(F3, Y) {
|
|
906
|
+
this[F3] = sF.bind(null, Y);
|
|
907
|
+
}
|
|
908
|
+
var oF = (F3, Y) => {
|
|
909
|
+
for (var B3 in Y)
|
|
910
|
+
rF(F3, B3, { get: Y[B3], enumerable: true, configurable: true, set: aF.bind(Y, B3) });
|
|
911
|
+
};
|
|
912
|
+
var E2 = {};
|
|
913
|
+
oF(E2, { yellow: () => o, white: () => FF, underline: () => c, strikethrough: () => d, reset: () => p, red: () => s, magenta: () => t, italic: () => h4, inverse: () => n, hidden: () => l, green: () => a, gray: () => BF, dim: () => m, cyan: () => e, brightYellow: () => ZF, brightWhite: () => JF, brightRed: () => YF, brightMagenta: () => zF, brightGreen: () => VF, brightCyan: () => QF, brightBlue: () => $F, bold: () => u, blue: () => i, black: () => r, bgYellow: () => GF, bgWhite: () => OF, bgRed: () => KF, bgMagenta: () => HF, bgGreen: () => jF, bgCyan: () => _F, bgBrightYellow: () => AF, bgBrightWhite: () => TF, bgBrightRed: () => qF, bgBrightMagenta: () => MF, bgBrightGreen: () => RF, bgBrightCyan: () => IF, bgBrightBlue: () => LF, bgBrightBlack: () => DF, bgBlue: () => UF, bgBlack: () => XF });
|
|
914
|
+
function J(F3, Y) {
|
|
915
|
+
return { open: `\x1B[${F3}m`, close: `\x1B[${Y}m` };
|
|
916
|
+
}
|
|
917
|
+
var p = J(0, 0);
|
|
918
|
+
var u = J(1, 22);
|
|
919
|
+
var m = J(2, 22);
|
|
920
|
+
var h4 = J(3, 23);
|
|
921
|
+
var c = J(4, 24);
|
|
922
|
+
var n = J(7, 27);
|
|
923
|
+
var l = J(8, 28);
|
|
924
|
+
var d = J(9, 29);
|
|
925
|
+
var r = J(30, 39);
|
|
926
|
+
var s = J(31, 39);
|
|
927
|
+
var a = J(32, 39);
|
|
928
|
+
var o = J(33, 39);
|
|
929
|
+
var i = J(34, 39);
|
|
930
|
+
var t = J(35, 39);
|
|
931
|
+
var e = J(36, 39);
|
|
932
|
+
var FF = J(37, 39);
|
|
933
|
+
var BF = J(90, 39);
|
|
934
|
+
var YF = J(91, 39);
|
|
935
|
+
var VF = J(92, 39);
|
|
936
|
+
var ZF = J(93, 39);
|
|
937
|
+
var $F = J(94, 39);
|
|
938
|
+
var zF = J(95, 39);
|
|
939
|
+
var QF = J(96, 39);
|
|
940
|
+
var JF = J(97, 39);
|
|
941
|
+
var XF = J(40, 49);
|
|
942
|
+
var KF = J(41, 49);
|
|
943
|
+
var jF = J(42, 49);
|
|
944
|
+
var GF = J(43, 49);
|
|
945
|
+
var UF = J(44, 49);
|
|
946
|
+
var HF = J(45, 49);
|
|
947
|
+
var _F = J(46, 49);
|
|
948
|
+
var OF = J(47, 49);
|
|
949
|
+
var DF = J(100, 49);
|
|
950
|
+
var qF = J(101, 49);
|
|
951
|
+
var RF = J(102, 49);
|
|
952
|
+
var AF = J(103, 49);
|
|
953
|
+
var LF = J(104, 49);
|
|
954
|
+
var MF = J(105, 49);
|
|
955
|
+
var IF = J(106, 49);
|
|
956
|
+
var TF = J(107, 49);
|
|
957
|
+
function iF(F3) {
|
|
958
|
+
return F3 >= 19968 && F3 <= 40959 || F3 >= 13312 && F3 <= 19903 || F3 >= 131072 && F3 <= 173791 || F3 >= 63744 && F3 <= 64255 || F3 >= 65281 && F3 <= 65376 || F3 >= 65504 && F3 <= 65510 || F3 >= 11904 && F3 <= 12031 || F3 >= 12032 && F3 <= 12255 || F3 >= 12288 && F3 <= 12351 || F3 >= 12352 && F3 <= 12447 || F3 >= 12448 && F3 <= 12543 || F3 >= 12544 && F3 <= 12591 || F3 >= 12592 && F3 <= 12687 || F3 >= 12800 && F3 <= 13055 || F3 >= 13056 && F3 <= 13311 || F3 >= 44032 && F3 <= 55215 || F3 >= 65072 && F3 <= 65103;
|
|
959
|
+
}
|
|
960
|
+
function O2(F3) {
|
|
961
|
+
let Y = Bun.stripANSI(F3), B3 = 0;
|
|
962
|
+
for (let V of Y) {
|
|
963
|
+
let Z = V.codePointAt(0);
|
|
964
|
+
if (Z === undefined)
|
|
965
|
+
continue;
|
|
966
|
+
B3 += iF(Z) ? 2 : 1;
|
|
967
|
+
}
|
|
968
|
+
return B3;
|
|
969
|
+
}
|
|
970
|
+
function M3(F3, Y) {
|
|
971
|
+
if (F3 === "always")
|
|
972
|
+
return true;
|
|
973
|
+
if (F3 === "never")
|
|
974
|
+
return false;
|
|
975
|
+
let V = Y !== undefined && "isTTY" in Y ? Y.isTTY ?? false : process.stdout?.isTTY ?? false, z3 = Y !== undefined && "noColor" in Y ? Y.noColor : process.env.NO_COLOR;
|
|
976
|
+
if (z3 !== undefined && z3 !== "")
|
|
977
|
+
return false;
|
|
978
|
+
return V;
|
|
979
|
+
}
|
|
980
|
+
function NF(F3, Y) {
|
|
981
|
+
if (F3 === "always")
|
|
982
|
+
return true;
|
|
983
|
+
if (F3 === "never")
|
|
984
|
+
return false;
|
|
985
|
+
return Y !== undefined && "isTTY" in Y ? Y.isTTY ?? false : process.stdout?.isTTY ?? false;
|
|
986
|
+
}
|
|
987
|
+
function kF(F3, Y) {
|
|
988
|
+
if (F3 === "always")
|
|
989
|
+
return true;
|
|
990
|
+
if (F3 === "never")
|
|
991
|
+
return false;
|
|
992
|
+
return Y !== undefined && "isTTY" in Y ? Y.isTTY ?? false : process.stdout?.isTTY ?? false;
|
|
993
|
+
}
|
|
994
|
+
function S2(F3, Y) {
|
|
995
|
+
if (F3 === "always")
|
|
996
|
+
return true;
|
|
997
|
+
if (F3 === "never")
|
|
998
|
+
return false;
|
|
999
|
+
if (!M3(F3, Y))
|
|
1000
|
+
return false;
|
|
1001
|
+
let Z = (Y !== undefined && "colorTerm" in Y ? Y.colorTerm : process.env.COLORTERM)?.toLowerCase();
|
|
1002
|
+
if (Z === "truecolor" || Z === "24bit")
|
|
1003
|
+
return true;
|
|
1004
|
+
let $ = Y !== undefined && "term" in Y ? Y.term : process.env.TERM;
|
|
1005
|
+
if ($ !== undefined) {
|
|
1006
|
+
let K = $.toLowerCase();
|
|
1007
|
+
if (K.includes("24bit") || K.includes("truecolor") || K.endsWith("-direct"))
|
|
1008
|
+
return true;
|
|
1009
|
+
}
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
function D3(F3, Y) {
|
|
1013
|
+
if (F3 === "")
|
|
1014
|
+
return "";
|
|
1015
|
+
let { open: B3, close: V } = Y;
|
|
1016
|
+
if (F3.includes(V))
|
|
1017
|
+
F3 = F3.replaceAll(V, V + B3);
|
|
1018
|
+
return B3 + F3 + V;
|
|
1019
|
+
}
|
|
1020
|
+
function f(F3, Y) {
|
|
1021
|
+
if (!Number.isInteger(F3) || F3 < 0 || F3 > 255)
|
|
1022
|
+
throw RangeError(`Invalid ${Y} value: ${String(F3)}. Must be an integer between 0 and 255.`);
|
|
1023
|
+
}
|
|
1024
|
+
function gF(F3, Y, B3) {
|
|
1025
|
+
f(F3, "red"), f(Y, "green"), f(B3, "blue");
|
|
1026
|
+
}
|
|
1027
|
+
var zB = /^#([0-9a-f]{3})$/i;
|
|
1028
|
+
var QB = /^#([0-9a-f]{6})$/i;
|
|
1029
|
+
function y3(F3) {
|
|
1030
|
+
let Y = zB.exec(F3);
|
|
1031
|
+
if (Y) {
|
|
1032
|
+
let V = Y[1], Z = V.charAt(0), z3 = V.charAt(1), $ = V.charAt(2);
|
|
1033
|
+
return [Number.parseInt(Z + Z, 16), Number.parseInt(z3 + z3, 16), Number.parseInt($ + $, 16)];
|
|
1034
|
+
}
|
|
1035
|
+
let B3 = QB.exec(F3);
|
|
1036
|
+
if (B3) {
|
|
1037
|
+
let V = B3[1];
|
|
1038
|
+
return [Number.parseInt(V.slice(0, 2), 16), Number.parseInt(V.slice(2, 4), 16), Number.parseInt(V.slice(4, 6), 16)];
|
|
1039
|
+
}
|
|
1040
|
+
throw TypeError(`Invalid hex color: "${F3}". Expected format: "#RGB" or "#RRGGBB".`);
|
|
1041
|
+
}
|
|
1042
|
+
function w3(F3, Y, B3) {
|
|
1043
|
+
return gF(F3, Y, B3), { open: `\x1B[38;2;${F3};${Y};${B3}m`, close: "\x1B[39m" };
|
|
1044
|
+
}
|
|
1045
|
+
function C(F3, Y, B3) {
|
|
1046
|
+
return gF(F3, Y, B3), { open: `\x1B[48;2;${F3};${Y};${B3}m`, close: "\x1B[49m" };
|
|
1047
|
+
}
|
|
1048
|
+
function WF(F3) {
|
|
1049
|
+
let [Y, B3, V] = y3(F3);
|
|
1050
|
+
return w3(Y, B3, V);
|
|
1051
|
+
}
|
|
1052
|
+
function SF(F3) {
|
|
1053
|
+
let [Y, B3, V] = y3(F3);
|
|
1054
|
+
return C(Y, B3, V);
|
|
1055
|
+
}
|
|
1056
|
+
function fF(F3, Y, B3, V) {
|
|
1057
|
+
return D3(F3, w3(Y, B3, V));
|
|
1058
|
+
}
|
|
1059
|
+
function yF(F3, Y, B3, V) {
|
|
1060
|
+
return D3(F3, C(Y, B3, V));
|
|
1061
|
+
}
|
|
1062
|
+
function wF(F3, Y) {
|
|
1063
|
+
return D3(F3, WF(Y));
|
|
1064
|
+
}
|
|
1065
|
+
function CF(F3, Y) {
|
|
1066
|
+
return D3(F3, SF(Y));
|
|
1067
|
+
}
|
|
1068
|
+
var xF = "\x1B]";
|
|
1069
|
+
var bF = "\x1B\\";
|
|
1070
|
+
var JB = `${xF}8;;${bF}`;
|
|
1071
|
+
var XB = /^[\x20-\x7e]*$/;
|
|
1072
|
+
var KB = /^[\x21-\x7e]*$/;
|
|
1073
|
+
function jB(F3, Y) {
|
|
1074
|
+
if (!XB.test(F3))
|
|
1075
|
+
throw TypeError(`Invalid ${Y}: must contain only printable ASCII characters.`);
|
|
1076
|
+
}
|
|
1077
|
+
function GB(F3, Y) {
|
|
1078
|
+
if (!KB.test(F3))
|
|
1079
|
+
throw TypeError(`Invalid ${Y}: must contain only printable ASCII characters without spaces.`);
|
|
1080
|
+
}
|
|
1081
|
+
function UB(F3) {
|
|
1082
|
+
let Y = F3?.id;
|
|
1083
|
+
if (Y === undefined || Y === "")
|
|
1084
|
+
return "";
|
|
1085
|
+
if (jB(Y, "hyperlink id"), Y.includes(":") || Y.includes(";"))
|
|
1086
|
+
throw TypeError('Invalid hyperlink id: ":" and ";" are reserved by the OSC 8 format.');
|
|
1087
|
+
return `id=${Y}`;
|
|
1088
|
+
}
|
|
1089
|
+
function PF(F3, Y) {
|
|
1090
|
+
GB(F3, "hyperlink URL");
|
|
1091
|
+
let B3 = UB(Y);
|
|
1092
|
+
return { open: `${xF}8;${B3};${F3}${bF}`, close: JB };
|
|
1093
|
+
}
|
|
1094
|
+
function vF(F3, Y, B3) {
|
|
1095
|
+
return D3(F3, PF(Y, B3));
|
|
1096
|
+
}
|
|
1097
|
+
function HB() {
|
|
1098
|
+
let { reset: F3, ...Y } = E2;
|
|
1099
|
+
return Y;
|
|
1100
|
+
}
|
|
1101
|
+
var x3 = Object.freeze(HB());
|
|
1102
|
+
var I4 = Object.freeze(Object.keys(x3));
|
|
1103
|
+
function pF(F3) {
|
|
1104
|
+
return x3[F3];
|
|
1105
|
+
}
|
|
1106
|
+
var uF = Object.freeze(["bold", "dim", "italic", "underline", "inverse", "hidden", "strikethrough"]);
|
|
1107
|
+
var _B = new Set(uF);
|
|
1108
|
+
function mF(F3) {
|
|
1109
|
+
return _B.has(F3);
|
|
1110
|
+
}
|
|
1111
|
+
var OB = new Set(uF.map((F3) => x3[F3]));
|
|
1112
|
+
function hF(F3) {
|
|
1113
|
+
return OB.has(F3);
|
|
1114
|
+
}
|
|
1115
|
+
function DB(F3, Y, B3, V) {
|
|
1116
|
+
if (F3 === "")
|
|
1117
|
+
return F3;
|
|
1118
|
+
let Z = F3;
|
|
1119
|
+
for (let z3 = Y.length - 1;z3 >= 0; z3--) {
|
|
1120
|
+
let $ = Y[z3];
|
|
1121
|
+
if ($ === undefined)
|
|
1122
|
+
continue;
|
|
1123
|
+
if (mF($) ? !B3 : !V)
|
|
1124
|
+
continue;
|
|
1125
|
+
Z = D3(Z, pF($));
|
|
1126
|
+
}
|
|
1127
|
+
return Z;
|
|
1128
|
+
}
|
|
1129
|
+
function qB(F3, Y) {
|
|
1130
|
+
let B3 = new Map;
|
|
1131
|
+
function V(z3) {
|
|
1132
|
+
return z3.join("|");
|
|
1133
|
+
}
|
|
1134
|
+
function Z(z3) {
|
|
1135
|
+
let $ = V(z3), K = B3.get($);
|
|
1136
|
+
if (K)
|
|
1137
|
+
return K;
|
|
1138
|
+
let j = (U2) => DB(U2, z3, F3, Y);
|
|
1139
|
+
B3.set($, j);
|
|
1140
|
+
for (let U2 of I4)
|
|
1141
|
+
Object.defineProperty(j, U2, { configurable: false, enumerable: true, get() {
|
|
1142
|
+
return Z([...z3, U2]);
|
|
1143
|
+
} });
|
|
1144
|
+
return Object.freeze(j);
|
|
1145
|
+
}
|
|
1146
|
+
return Z;
|
|
1147
|
+
}
|
|
1148
|
+
function RB(F3) {
|
|
1149
|
+
let Y = {};
|
|
1150
|
+
for (let B3 of I4)
|
|
1151
|
+
Y[B3] = F3([B3]);
|
|
1152
|
+
return Y;
|
|
1153
|
+
}
|
|
1154
|
+
function A4(F3) {
|
|
1155
|
+
let Y = F3?.mode ?? "auto", B3 = NF(Y, F3?.overrides), V = M3(Y, F3?.overrides), Z = kF(Y, F3?.overrides), z3 = B3 || V, $ = S2(Y, F3?.overrides), K = qB(B3, V), j = RB(K), U2 = { enabled: z3, colorsEnabled: V, trueColorEnabled: $, apply: (X2, G) => {
|
|
1156
|
+
return (hF(G) ? B3 : V) ? D3(X2, G) : X2;
|
|
1157
|
+
}, link: Z ? (X2, G, _5) => vF(X2, G, _5) : (X2, G) => X2, rgb: $ ? (X2, G, _5, H) => fF(X2, G, _5, H) : (X2, G, _5, H) => X2, bgRgb: $ ? (X2, G, _5, H) => yF(X2, G, _5, H) : (X2, G, _5, H) => X2, hex: $ ? (X2, G) => wF(X2, G) : (X2, G) => X2, bgHex: $ ? (X2, G) => CF(X2, G) : (X2, G) => X2, ...j };
|
|
1158
|
+
return Object.freeze(U2);
|
|
1159
|
+
}
|
|
1160
|
+
var L;
|
|
1161
|
+
var cF = new Map;
|
|
1162
|
+
function MB() {
|
|
1163
|
+
if (L === "always")
|
|
1164
|
+
return A4({ mode: "always" });
|
|
1165
|
+
if (L === "never")
|
|
1166
|
+
return A4({ mode: "auto", overrides: { isTTY: true, noColor: "1" } });
|
|
1167
|
+
return A4({ mode: "auto" });
|
|
1168
|
+
}
|
|
1169
|
+
function b4() {
|
|
1170
|
+
let F3 = `${L ?? "auto"}|${process.stdout?.isTTY ?? false}|${process.env.NO_COLOR ?? ""}`, Y = cF.get(F3);
|
|
1171
|
+
if (Y)
|
|
1172
|
+
return Y;
|
|
1173
|
+
let B3 = MB();
|
|
1174
|
+
return cF.set(F3, B3), B3;
|
|
1175
|
+
}
|
|
1176
|
+
var IB = ["apply", "link", "rgb", "bgRgb", "hex", "bgHex"];
|
|
1177
|
+
var TB = ["enabled", "colorsEnabled", "trueColorEnabled"];
|
|
1178
|
+
function EB() {
|
|
1179
|
+
let F3 = {};
|
|
1180
|
+
for (let Y of TB)
|
|
1181
|
+
Object.defineProperty(F3, Y, { configurable: false, enumerable: true, get() {
|
|
1182
|
+
return b4()[Y];
|
|
1183
|
+
} });
|
|
1184
|
+
for (let Y of IB)
|
|
1185
|
+
Object.defineProperty(F3, Y, { configurable: false, enumerable: true, value: (...B3) => {
|
|
1186
|
+
return b4()[Y](...B3);
|
|
1187
|
+
}, writable: false });
|
|
1188
|
+
for (let Y of I4)
|
|
1189
|
+
Object.defineProperty(F3, Y, { configurable: false, enumerable: true, get() {
|
|
1190
|
+
return b4()[Y];
|
|
1191
|
+
} });
|
|
1192
|
+
return Object.freeze(F3);
|
|
1193
|
+
}
|
|
1194
|
+
var Q = EB();
|
|
1195
|
+
var kB = (F3) => Q.red(F3);
|
|
1196
|
+
var gB = (F3) => Q.green(F3);
|
|
1197
|
+
var yB = (F3) => Q.cyan(F3);
|
|
1198
|
+
var ZY = (F3) => Q.bold(F3);
|
|
1199
|
+
var $Y = (F3) => Q.dim(F3);
|
|
1200
|
+
function P4(F3) {
|
|
1201
|
+
return Object.freeze({ heading1: (B3) => F3.bold(F3.underline(B3)), heading2: (B3) => F3.bold(B3), heading3: (B3) => F3.bold(F3.yellow(B3)), heading4: (B3) => F3.yellow(B3), heading5: (B3) => F3.dim(F3.yellow(B3)), heading6: (B3) => F3.dim(B3), text: (B3) => B3, emphasis: (B3) => F3.italic(B3), strong: (B3) => F3.bold(B3), strongEmphasis: (B3) => F3.bold(F3.italic(B3)), strikethrough: (B3) => F3.strikethrough(B3), inlineCode: (B3) => F3.cyan(B3), linkText: (B3) => F3.blue(F3.underline(B3)), linkUrl: (B3) => F3.dim(F3.underline(B3)), autolink: (B3) => F3.blue(F3.underline(B3)), blockquoteMarker: (B3) => F3.dim(F3.green(B3)), blockquoteText: (B3) => F3.italic(B3), listMarker: (B3) => F3.dim(B3), orderedListMarker: (B3) => F3.dim(B3), taskChecked: (B3) => F3.green(B3), taskUnchecked: (B3) => F3.dim(B3), codeFence: (B3) => F3.dim(B3), codeInfo: (B3) => F3.dim(F3.italic(B3)), codeText: (B3) => F3.cyan(B3), thematicBreak: (B3) => F3.dim(B3), tableHeader: (B3) => F3.bold(B3), tableCell: (B3) => B3, tableBorder: (B3) => F3.dim(B3), imageAltText: (B3) => F3.italic(F3.magenta(B3)), imageUrl: (B3) => F3.dim(F3.underline(B3)) });
|
|
1202
|
+
}
|
|
1203
|
+
function nF(F3) {
|
|
1204
|
+
let Y = A4(F3?.style), B3 = P4(Y), V = F3?.overrides;
|
|
1205
|
+
if (!V)
|
|
1206
|
+
return B3;
|
|
1207
|
+
let Z = { heading1: V.heading1 ?? B3.heading1, heading2: V.heading2 ?? B3.heading2, heading3: V.heading3 ?? B3.heading3, heading4: V.heading4 ?? B3.heading4, heading5: V.heading5 ?? B3.heading5, heading6: V.heading6 ?? B3.heading6, text: V.text ?? B3.text, emphasis: V.emphasis ?? B3.emphasis, strong: V.strong ?? B3.strong, strongEmphasis: V.strongEmphasis ?? B3.strongEmphasis, strikethrough: V.strikethrough ?? B3.strikethrough, inlineCode: V.inlineCode ?? B3.inlineCode, linkText: V.linkText ?? B3.linkText, linkUrl: V.linkUrl ?? B3.linkUrl, autolink: V.autolink ?? B3.autolink, blockquoteMarker: V.blockquoteMarker ?? B3.blockquoteMarker, blockquoteText: V.blockquoteText ?? B3.blockquoteText, listMarker: V.listMarker ?? B3.listMarker, orderedListMarker: V.orderedListMarker ?? B3.orderedListMarker, taskChecked: V.taskChecked ?? B3.taskChecked, taskUnchecked: V.taskUnchecked ?? B3.taskUnchecked, codeFence: V.codeFence ?? B3.codeFence, codeInfo: V.codeInfo ?? B3.codeInfo, codeText: V.codeText ?? B3.codeText, thematicBreak: V.thematicBreak ?? B3.thematicBreak, tableHeader: V.tableHeader ?? B3.tableHeader, tableCell: V.tableCell ?? B3.tableCell, tableBorder: V.tableBorder ?? B3.tableBorder, imageAltText: V.imageAltText ?? B3.imageAltText, imageUrl: V.imageUrl ?? B3.imageUrl };
|
|
1208
|
+
return Object.freeze(Z);
|
|
1209
|
+
}
|
|
1210
|
+
var MY = nF();
|
|
1211
|
+
|
|
1212
|
+
// ../../node_modules/.bun/@crustjs+prompts@0.0.13+7524df1edfed9f02/node_modules/@crustjs/prompts/dist/index.js
|
|
1213
|
+
import * as QJ from "readline";
|
|
1214
|
+
|
|
1215
|
+
// ../../node_modules/.bun/@crustjs+progress@0.0.3+7524df1edfed9f02/node_modules/@crustjs/progress/dist/index.js
|
|
1216
|
+
var Q2 = "\x1B[";
|
|
1217
|
+
var N3 = `${Q2}?25l`;
|
|
1218
|
+
var J2 = `${Q2}?25h`;
|
|
1219
|
+
var V = `${Q2}2K`;
|
|
1220
|
+
|
|
1221
|
+
// ../../node_modules/.bun/@crustjs+prompts@0.0.13+7524df1edfed9f02/node_modules/@crustjs/prompts/dist/index.js
|
|
1222
|
+
var b5 = { prefix: yB, message: ZY, placeholder: $Y, cursor: yB, selected: yB, unselected: $Y, error: kB, success: gB, hint: $Y, filterMatch: yB };
|
|
1223
|
+
var h5;
|
|
1224
|
+
function w4(J3) {
|
|
1225
|
+
if (!h5 && !J3)
|
|
1226
|
+
return b5;
|
|
1227
|
+
return { ...b5, ...h5, ...J3 };
|
|
1228
|
+
}
|
|
1229
|
+
var i2 = Symbol("submit");
|
|
1230
|
+
function M4(J3) {
|
|
1231
|
+
return { [i2]: J3 };
|
|
1232
|
+
}
|
|
1233
|
+
var o2 = false;
|
|
1234
|
+
var u2 = "\x1B[";
|
|
1235
|
+
var zJ = `${u2}?25l`;
|
|
1236
|
+
var NJ = `${u2}?25h`;
|
|
1237
|
+
var qJ = `${u2}2K`;
|
|
1238
|
+
function e2(J3) {
|
|
1239
|
+
return J3 > 0 ? `${u2}${J3}A` : "";
|
|
1240
|
+
}
|
|
1241
|
+
function JJ(J3, Q3) {
|
|
1242
|
+
let Z = J3.split(`
|
|
1243
|
+
`), W2 = 0;
|
|
1244
|
+
for (let G of Z) {
|
|
1245
|
+
let $ = O2(G);
|
|
1246
|
+
W2 += $ === 0 ? 1 : Math.ceil($ / Q3);
|
|
1247
|
+
}
|
|
1248
|
+
return W2;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
class a2 extends Error {
|
|
1252
|
+
constructor(J3) {
|
|
1253
|
+
super(J3 ?? "Prompts require an interactive terminal (TTY).");
|
|
1254
|
+
this.name = "NonInteractiveError";
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
class s2 extends Error {
|
|
1259
|
+
constructor(J3) {
|
|
1260
|
+
super(J3 ?? "Prompt was cancelled.");
|
|
1261
|
+
this.name = "CancelledError";
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
function _5() {
|
|
1265
|
+
return !!process.stdin.isTTY;
|
|
1266
|
+
}
|
|
1267
|
+
function ZJ() {
|
|
1268
|
+
if (!_5())
|
|
1269
|
+
throw new a2;
|
|
1270
|
+
}
|
|
1271
|
+
function HJ(J3) {
|
|
1272
|
+
return typeof J3 === "object" && J3 !== null && i2 in J3;
|
|
1273
|
+
}
|
|
1274
|
+
function D4(J3) {
|
|
1275
|
+
let { render: Q3, handleKey: Z, initialState: W2, theme: G, renderSubmitted: $ } = J3;
|
|
1276
|
+
return new Promise((K, Y) => {
|
|
1277
|
+
if (o2) {
|
|
1278
|
+
Y(Error("Cannot run multiple prompts concurrently. Await each prompt before starting the next."));
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
try {
|
|
1282
|
+
ZJ();
|
|
1283
|
+
} catch (U2) {
|
|
1284
|
+
Y(U2);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
o2 = true;
|
|
1288
|
+
let j = W2, X2 = 0, F3 = false, B3 = process.stdin, z3 = process.stderr;
|
|
1289
|
+
function N4() {
|
|
1290
|
+
if (F3)
|
|
1291
|
+
return;
|
|
1292
|
+
if (F3 = true, o2 = false, B3.removeListener("keypress", x4), B3.isTTY && B3.isRaw)
|
|
1293
|
+
B3.setRawMode(false);
|
|
1294
|
+
B3.pause(), z3.write(NJ);
|
|
1295
|
+
}
|
|
1296
|
+
function H(U2) {
|
|
1297
|
+
let T3 = z3.columns || 80;
|
|
1298
|
+
if (X2 > 0) {
|
|
1299
|
+
z3.write(`${e2(X2 - 1)}\r`);
|
|
1300
|
+
for (let p2 = 0;p2 < X2; p2++)
|
|
1301
|
+
if (z3.write(qJ), p2 < X2 - 1)
|
|
1302
|
+
z3.write(`${u2}1B`);
|
|
1303
|
+
if (X2 > 1)
|
|
1304
|
+
z3.write(e2(X2 - 1));
|
|
1305
|
+
z3.write("\r");
|
|
1306
|
+
}
|
|
1307
|
+
z3.write(U2), X2 = JJ(U2, T3);
|
|
1308
|
+
}
|
|
1309
|
+
let P5 = Promise.resolve(), R3 = null;
|
|
1310
|
+
function I5() {
|
|
1311
|
+
if (R3 !== null)
|
|
1312
|
+
return;
|
|
1313
|
+
R3 = setTimeout(() => {
|
|
1314
|
+
if (R3 = null, !F3)
|
|
1315
|
+
H(Q3(j, G));
|
|
1316
|
+
}, 0);
|
|
1317
|
+
}
|
|
1318
|
+
function y4() {
|
|
1319
|
+
if (R3 !== null)
|
|
1320
|
+
clearTimeout(R3), R3 = null;
|
|
1321
|
+
}
|
|
1322
|
+
function x4(U2, T3) {
|
|
1323
|
+
if (T3?.ctrl && T3.name === "c") {
|
|
1324
|
+
y4(), z3.write(`
|
|
1325
|
+
`), N4(), Y(new s2);
|
|
1326
|
+
return;
|
|
1327
|
+
}
|
|
1328
|
+
let p2 = { char: U2 ?? "", name: T3?.name ?? "", ctrl: T3?.ctrl ?? false, meta: T3?.meta ?? false, shift: T3?.shift ?? false };
|
|
1329
|
+
P5 = P5.then(async () => {
|
|
1330
|
+
if (F3)
|
|
1331
|
+
return;
|
|
1332
|
+
try {
|
|
1333
|
+
let d2 = await Z(p2, j);
|
|
1334
|
+
if (HJ(d2)) {
|
|
1335
|
+
y4();
|
|
1336
|
+
let t2 = d2[i2];
|
|
1337
|
+
if ($)
|
|
1338
|
+
H($(j, t2, G));
|
|
1339
|
+
z3.write(`
|
|
1340
|
+
`), N4(), K(t2);
|
|
1341
|
+
} else
|
|
1342
|
+
j = d2, I5();
|
|
1343
|
+
} catch (d2) {
|
|
1344
|
+
y4(), N4(), Y(d2);
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
try {
|
|
1349
|
+
QJ.emitKeypressEvents(B3), B3.setRawMode(true), B3.resume(), z3.write(zJ);
|
|
1350
|
+
let U2 = Q3(j, G);
|
|
1351
|
+
z3.write(U2);
|
|
1352
|
+
let T3 = z3.columns || 80;
|
|
1353
|
+
X2 = JJ(U2, T3), B3.on("keypress", x4);
|
|
1354
|
+
} catch (U2) {
|
|
1355
|
+
N4(), Y(U2);
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
var V2 = "┃";
|
|
1360
|
+
var E3 = "✓";
|
|
1361
|
+
function c2(J3, Q3) {
|
|
1362
|
+
if (Q3)
|
|
1363
|
+
return `${J3} ${Q3}`;
|
|
1364
|
+
return J3;
|
|
1365
|
+
}
|
|
1366
|
+
function A5(J3, Q3, Z) {
|
|
1367
|
+
let W2 = [J3];
|
|
1368
|
+
if (Q3)
|
|
1369
|
+
W2.push(Q3);
|
|
1370
|
+
if (Z)
|
|
1371
|
+
W2.push(Z);
|
|
1372
|
+
return W2.join(" ");
|
|
1373
|
+
}
|
|
1374
|
+
function L2(J3, Q3, Z, W2) {
|
|
1375
|
+
let G = c2(J3, Q3), $ = W2 ?? "";
|
|
1376
|
+
if (Q3)
|
|
1377
|
+
return `${G}${$}
|
|
1378
|
+
${Z}`;
|
|
1379
|
+
return `${G}${$} ${Z}`;
|
|
1380
|
+
}
|
|
1381
|
+
function MJ(J3, Q3) {
|
|
1382
|
+
if (J3.name === "return")
|
|
1383
|
+
return M4(Q3.value);
|
|
1384
|
+
if (J3.name === "left" || J3.name === "right")
|
|
1385
|
+
return { value: !Q3.value };
|
|
1386
|
+
if (J3.name === "h")
|
|
1387
|
+
return { value: true };
|
|
1388
|
+
if (J3.name === "l")
|
|
1389
|
+
return { value: false };
|
|
1390
|
+
if (J3.name === "tab")
|
|
1391
|
+
return { value: !Q3.value };
|
|
1392
|
+
if (J3.char === "y" || J3.char === "Y")
|
|
1393
|
+
return { value: true };
|
|
1394
|
+
if (J3.char === "n" || J3.char === "N")
|
|
1395
|
+
return { value: false };
|
|
1396
|
+
return Q3;
|
|
1397
|
+
}
|
|
1398
|
+
var DJ = " · ";
|
|
1399
|
+
function AJ(J3, Q3, Z, W2, G) {
|
|
1400
|
+
let $ = Q3.prefix(V2), K = Q3.message(Z ?? "Are you sure?"), Y = J3.value ? `${Q3.selected("●")} ${Q3.selected(W2)}` : `${Q3.unselected("○")} ${Q3.unselected(W2)}`, j = J3.value ? `${Q3.unselected("○")} ${Q3.unselected(G)}` : `${Q3.selected("●")} ${Q3.selected(G)}`, X2 = `${Y}${DJ}${j}`;
|
|
1401
|
+
return L2($, K, X2);
|
|
1402
|
+
}
|
|
1403
|
+
function UJ(J3, Q3, Z, W2, G, $) {
|
|
1404
|
+
let K = Z.success(E3), Y = Z.message(W2 ?? "Are you sure?"), j = Q3 ? G : $;
|
|
1405
|
+
return A5(K, Y, Z.success(j));
|
|
1406
|
+
}
|
|
1407
|
+
async function wJ(J3) {
|
|
1408
|
+
if (J3.initial !== undefined)
|
|
1409
|
+
return J3.initial;
|
|
1410
|
+
if (!_5() && J3.default !== undefined)
|
|
1411
|
+
return J3.default;
|
|
1412
|
+
let Q3 = w4(J3.theme), Z = J3.active ?? "Yes", W2 = J3.inactive ?? "No", $ = { value: J3.default ?? true };
|
|
1413
|
+
return D4({ initialState: $, theme: Q3, render: (K, Y) => AJ(K, Y, J3.message, Z, W2), handleKey: MJ, renderSubmitted: (K, Y, j) => UJ(K, Y, j, J3.message, Z, W2) });
|
|
1414
|
+
}
|
|
1415
|
+
|
|
902
1416
|
// src/commands/scan.ts
|
|
903
|
-
import { writeFileSync } from "node:fs";
|
|
1417
|
+
import { lstatSync, mkdirSync as mkdirSync2, readdirSync as readdirSync4, statSync as statSync5, writeFileSync as writeFileSync2 } from "node:fs";
|
|
1418
|
+
import { join as join12 } from "node:path";
|
|
904
1419
|
|
|
905
1420
|
// src/sources/github.ts
|
|
906
1421
|
import { spawnSync } from "node:child_process";
|
|
@@ -998,20 +1513,6 @@ async function resolveSource(input) {
|
|
|
998
1513
|
return resolveGithub(parsed.repo);
|
|
999
1514
|
}
|
|
1000
1515
|
|
|
1001
|
-
// src/detector/marketplaceGuard.ts
|
|
1002
|
-
import { existsSync } from "node:fs";
|
|
1003
|
-
import { join as join2 } from "node:path";
|
|
1004
|
-
function checkMarketplaceGuard(repoRoot) {
|
|
1005
|
-
const marketplaceFile = join2(repoRoot, ".claude-plugin", "marketplace.json");
|
|
1006
|
-
if (existsSync(marketplaceFile)) {
|
|
1007
|
-
throw new AlreadyMarketplaceError(repoRoot);
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
// src/detector/markerFile.ts
|
|
1012
|
-
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
1013
|
-
import { join as join3 } from "node:path";
|
|
1014
|
-
|
|
1015
1516
|
// ../../node_modules/.bun/valibot@1.3.1+7524df1edfed9f02/node_modules/valibot/dist/index.mjs
|
|
1016
1517
|
var store$4;
|
|
1017
1518
|
function getGlobalConfig(config$1) {
|
|
@@ -1091,6 +1592,28 @@ function _joinExpects(values$1, separator) {
|
|
|
1091
1592
|
return `(${list.join(` ${separator} `)})`;
|
|
1092
1593
|
return list[0] ?? "never";
|
|
1093
1594
|
}
|
|
1595
|
+
function getDotPath(issue) {
|
|
1596
|
+
if (issue.path) {
|
|
1597
|
+
let key = "";
|
|
1598
|
+
for (const item of issue.path)
|
|
1599
|
+
if (typeof item.key === "string" || typeof item.key === "number")
|
|
1600
|
+
if (key)
|
|
1601
|
+
key += `.${item.key}`;
|
|
1602
|
+
else
|
|
1603
|
+
key += item.key;
|
|
1604
|
+
else
|
|
1605
|
+
return null;
|
|
1606
|
+
return key;
|
|
1607
|
+
}
|
|
1608
|
+
return null;
|
|
1609
|
+
}
|
|
1610
|
+
var ValiError = class extends Error {
|
|
1611
|
+
constructor(issues) {
|
|
1612
|
+
super(issues[0].message);
|
|
1613
|
+
this.name = "ValiError";
|
|
1614
|
+
this.issues = issues;
|
|
1615
|
+
}
|
|
1616
|
+
};
|
|
1094
1617
|
function regex(requirement, message$1) {
|
|
1095
1618
|
return {
|
|
1096
1619
|
kind: "validation",
|
|
@@ -1123,6 +1646,19 @@ function startsWith(requirement, message$1) {
|
|
|
1123
1646
|
}
|
|
1124
1647
|
};
|
|
1125
1648
|
}
|
|
1649
|
+
function transform(operation) {
|
|
1650
|
+
return {
|
|
1651
|
+
kind: "transformation",
|
|
1652
|
+
type: "transform",
|
|
1653
|
+
reference: transform,
|
|
1654
|
+
async: false,
|
|
1655
|
+
operation,
|
|
1656
|
+
"~run"(dataset) {
|
|
1657
|
+
dataset.value = this.operation(dataset.value);
|
|
1658
|
+
return dataset;
|
|
1659
|
+
}
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1126
1662
|
var _LruCache = class {
|
|
1127
1663
|
constructor(config$1) {
|
|
1128
1664
|
this.refCount = 0;
|
|
@@ -1259,6 +1795,27 @@ function boolean(message$1) {
|
|
|
1259
1795
|
}
|
|
1260
1796
|
};
|
|
1261
1797
|
}
|
|
1798
|
+
function custom(check$1, message$1) {
|
|
1799
|
+
return {
|
|
1800
|
+
kind: "schema",
|
|
1801
|
+
type: "custom",
|
|
1802
|
+
reference: custom,
|
|
1803
|
+
expects: "unknown",
|
|
1804
|
+
async: false,
|
|
1805
|
+
check: check$1,
|
|
1806
|
+
message: message$1,
|
|
1807
|
+
get "~standard"() {
|
|
1808
|
+
return /* @__PURE__ */ _getStandardProps(this);
|
|
1809
|
+
},
|
|
1810
|
+
"~run"(dataset, config$1) {
|
|
1811
|
+
if (this.check(dataset.value))
|
|
1812
|
+
dataset.typed = true;
|
|
1813
|
+
else
|
|
1814
|
+
_addIssue(this, "type", dataset, config$1);
|
|
1815
|
+
return dataset;
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1262
1819
|
function literal(literal_, message$1) {
|
|
1263
1820
|
return {
|
|
1264
1821
|
kind: "schema",
|
|
@@ -1653,6 +2210,12 @@ function unknown() {
|
|
|
1653
2210
|
}
|
|
1654
2211
|
};
|
|
1655
2212
|
}
|
|
2213
|
+
function parse(schema, input, config$1) {
|
|
2214
|
+
const dataset = schema["~run"]({ value: input }, /* @__PURE__ */ getGlobalConfig(config$1));
|
|
2215
|
+
if (dataset.issues)
|
|
2216
|
+
throw new ValiError(dataset.issues);
|
|
2217
|
+
return dataset.value;
|
|
2218
|
+
}
|
|
1656
2219
|
function pipe(...pipe$1) {
|
|
1657
2220
|
return {
|
|
1658
2221
|
...pipe$1[0],
|
|
@@ -1684,35 +2247,238 @@ function safeParse(schema, input, config$1) {
|
|
|
1684
2247
|
};
|
|
1685
2248
|
}
|
|
1686
2249
|
|
|
2250
|
+
// src/detector/marketplaceGuard.ts
|
|
2251
|
+
import { existsSync } from "node:fs";
|
|
2252
|
+
import { join as join2 } from "node:path";
|
|
2253
|
+
function isAlreadyMarketplace(repoRoot) {
|
|
2254
|
+
return existsSync(join2(repoRoot, ".claude-plugin", "marketplace.json"));
|
|
2255
|
+
}
|
|
2256
|
+
function checkMarketplaceGuard(repoRoot) {
|
|
2257
|
+
if (isAlreadyMarketplace(repoRoot)) {
|
|
2258
|
+
throw new AlreadyMarketplaceError(repoRoot);
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
// src/detector/markerFile.ts
|
|
2263
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
2264
|
+
import { join as join4 } from "node:path";
|
|
2265
|
+
|
|
2266
|
+
// src/detector/fsWalk.ts
|
|
2267
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
2268
|
+
import { join as join3 } from "node:path";
|
|
2269
|
+
function isPermissionError(err) {
|
|
2270
|
+
const code = err?.code;
|
|
2271
|
+
return code === "EACCES" || code === "EPERM";
|
|
2272
|
+
}
|
|
2273
|
+
function makeDirLister(onSkip) {
|
|
2274
|
+
const cache = new Map;
|
|
2275
|
+
return (dir) => {
|
|
2276
|
+
const hit = cache.get(dir);
|
|
2277
|
+
if (hit !== undefined) {
|
|
2278
|
+
return hit;
|
|
2279
|
+
}
|
|
2280
|
+
let out = [];
|
|
2281
|
+
try {
|
|
2282
|
+
out = readdirSync(dir, { withFileTypes: true }).map((d2) => {
|
|
2283
|
+
if (d2.isSymbolicLink()) {
|
|
2284
|
+
try {
|
|
2285
|
+
const s3 = statSync(join3(dir, d2.name));
|
|
2286
|
+
return { name: d2.name, isDirectory: s3.isDirectory(), isFile: s3.isFile(), isSymlink: true };
|
|
2287
|
+
} catch {
|
|
2288
|
+
return { name: d2.name, isDirectory: false, isFile: false, isSymlink: true };
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return { name: d2.name, isDirectory: d2.isDirectory(), isFile: d2.isFile(), isSymlink: false };
|
|
2292
|
+
});
|
|
2293
|
+
} catch (err) {
|
|
2294
|
+
if (isPermissionError(err)) {
|
|
2295
|
+
onSkip?.(dir, err);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
cache.set(dir, out);
|
|
2299
|
+
return out;
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
function dirContainsFile(list, dir, name) {
|
|
2303
|
+
return list(dir).some((e3) => e3.isFile && e3.name === name);
|
|
2304
|
+
}
|
|
2305
|
+
function dirContainsDir(list, dir, name) {
|
|
2306
|
+
return list(dir).some((e3) => e3.isDirectory && e3.name === name);
|
|
2307
|
+
}
|
|
2308
|
+
function walkTree(root, options) {
|
|
2309
|
+
const list = options.list ?? makeDirLister();
|
|
2310
|
+
const realDirs = [];
|
|
2311
|
+
const pendingSymlinks = [];
|
|
2312
|
+
let seen = null;
|
|
2313
|
+
const idOf = (dir) => {
|
|
2314
|
+
try {
|
|
2315
|
+
const st = statSync(dir);
|
|
2316
|
+
return `${String(st.dev)}:${String(st.ino)}`;
|
|
2317
|
+
} catch {
|
|
2318
|
+
return null;
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2321
|
+
const walkInto = (dir) => {
|
|
2322
|
+
options.onDir?.(dir);
|
|
2323
|
+
const entries = list(dir).filter((e3) => !options.skipDirs.has(e3.name));
|
|
2324
|
+
for (const entry of entries) {
|
|
2325
|
+
if (entry.isFile) {
|
|
2326
|
+
options.onFile?.(join3(dir, entry.name));
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
for (const entry of entries) {
|
|
2330
|
+
if (entry.isDirectory && !entry.isSymlink) {
|
|
2331
|
+
const full = join3(dir, entry.name);
|
|
2332
|
+
realDirs.push(full);
|
|
2333
|
+
if (seen !== null) {
|
|
2334
|
+
const id = idOf(full);
|
|
2335
|
+
if (id === null || seen.has(id)) {
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
seen.add(id);
|
|
2339
|
+
}
|
|
2340
|
+
walkInto(full);
|
|
2341
|
+
} else if (entry.isDirectory && entry.isSymlink) {
|
|
2342
|
+
pendingSymlinks.push(join3(dir, entry.name));
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
};
|
|
2346
|
+
realDirs.push(root);
|
|
2347
|
+
walkInto(root);
|
|
2348
|
+
while (pendingSymlinks.length > 0) {
|
|
2349
|
+
seen ??= new Set(realDirs.map(idOf).filter((id2) => id2 !== null));
|
|
2350
|
+
const dir = pendingSymlinks.shift();
|
|
2351
|
+
if (dir === undefined) {
|
|
2352
|
+
break;
|
|
2353
|
+
}
|
|
2354
|
+
const id = idOf(dir);
|
|
2355
|
+
if (id === null || seen.has(id)) {
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
seen.add(id);
|
|
2359
|
+
walkInto(dir);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
function readJsonFile(file) {
|
|
2363
|
+
let raw;
|
|
2364
|
+
try {
|
|
2365
|
+
raw = readFileSync(file, "utf8");
|
|
2366
|
+
} catch (e3) {
|
|
2367
|
+
throw new Error(`Cannot read ${file}: ${e3 instanceof Error ? e3.message : String(e3)}`, { cause: e3 });
|
|
2368
|
+
}
|
|
2369
|
+
try {
|
|
2370
|
+
return JSON.parse(raw);
|
|
2371
|
+
} catch (e3) {
|
|
2372
|
+
throw new Error(`Invalid JSON in ${file}: ${e3 instanceof Error ? e3.message : String(e3)}`, {
|
|
2373
|
+
cause: e3
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
|
|
2378
|
+
// src/schemas/marketplaceEntry.ts
|
|
2379
|
+
var PathString = pipe(string(), startsWith("./"));
|
|
2380
|
+
var GithubSourceSchema = strictObject({
|
|
2381
|
+
source: literal("github"),
|
|
2382
|
+
repo: pipe(string(), regex(/^[\w.-]+\/[\w.-]+$/)),
|
|
2383
|
+
ref: optional(string()),
|
|
2384
|
+
sha: optional(string())
|
|
2385
|
+
});
|
|
2386
|
+
var UrlSourceSchema = strictObject({
|
|
2387
|
+
source: literal("url"),
|
|
2388
|
+
url: string(),
|
|
2389
|
+
ref: optional(string()),
|
|
2390
|
+
sha: optional(string())
|
|
2391
|
+
});
|
|
2392
|
+
var GitSubdirSourceSchema = strictObject({
|
|
2393
|
+
source: literal("git-subdir"),
|
|
2394
|
+
url: string(),
|
|
2395
|
+
path: string(),
|
|
2396
|
+
ref: optional(string()),
|
|
2397
|
+
sha: optional(string())
|
|
2398
|
+
});
|
|
2399
|
+
var SourceSchema = union([GithubSourceSchema, UrlSourceSchema, GitSubdirSourceSchema]);
|
|
2400
|
+
var DependencySchema = union([
|
|
2401
|
+
string(),
|
|
2402
|
+
object({ name: string(), version: optional(string()) })
|
|
2403
|
+
]);
|
|
2404
|
+
var NAME_REGEX = /^[a-z0-9][a-z0-9-]*$/;
|
|
2405
|
+
var MarketplaceEntrySchema = object({
|
|
2406
|
+
name: pipe(string(), regex(NAME_REGEX)),
|
|
2407
|
+
source: SourceSchema,
|
|
2408
|
+
strict: optional(boolean()),
|
|
2409
|
+
dependencies: optional(array(DependencySchema)),
|
|
2410
|
+
description: optional(string()),
|
|
2411
|
+
version: optional(string()),
|
|
2412
|
+
author: optional(union([string(), object({ name: string() })])),
|
|
2413
|
+
homepage: optional(string()),
|
|
2414
|
+
repository: optional(string()),
|
|
2415
|
+
license: optional(string()),
|
|
2416
|
+
keywords: optional(array(string())),
|
|
2417
|
+
skills: optional(array(PathString)),
|
|
2418
|
+
agents: optional(array(PathString)),
|
|
2419
|
+
commands: optional(array(PathString)),
|
|
2420
|
+
hooks: optional(union([PathString, record(string(), unknown())])),
|
|
2421
|
+
mcpServers: optional(union([PathString, record(string(), unknown())])),
|
|
2422
|
+
outputStyles: optional(array(PathString)),
|
|
2423
|
+
themes: optional(array(PathString)),
|
|
2424
|
+
monitors: optional(PathString)
|
|
2425
|
+
});
|
|
2426
|
+
|
|
1687
2427
|
// src/schemas/markerFile.ts
|
|
2428
|
+
var PathString2 = pipe(string(), startsWith("./"));
|
|
2429
|
+
var GroupSchema = strictObject({
|
|
2430
|
+
slug: pipe(string(), regex(NAME_REGEX)),
|
|
2431
|
+
skills: array(PathString2)
|
|
2432
|
+
});
|
|
1688
2433
|
var MarkerFileSchema = strictObject({
|
|
1689
|
-
name: pipe(string(), regex(
|
|
2434
|
+
name: pipe(string(), regex(NAME_REGEX)),
|
|
1690
2435
|
description: optional(string()),
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
2436
|
+
groups: optional(array(GroupSchema)),
|
|
2437
|
+
core: optional(boolean()),
|
|
2438
|
+
umbrella: optional(boolean()),
|
|
2439
|
+
skills: optional(array(PathString2)),
|
|
2440
|
+
agents: optional(array(PathString2)),
|
|
2441
|
+
commands: optional(array(PathString2)),
|
|
1694
2442
|
hooks: optional(string()),
|
|
1695
2443
|
mcpServers: optional(string()),
|
|
1696
|
-
outputStyles: optional(array(
|
|
1697
|
-
themes: optional(array(
|
|
2444
|
+
outputStyles: optional(array(PathString2)),
|
|
2445
|
+
themes: optional(array(PathString2)),
|
|
1698
2446
|
monitors: optional(string()),
|
|
1699
2447
|
license: optional(string()),
|
|
1700
2448
|
homepage: optional(string()),
|
|
1701
2449
|
repository: optional(string())
|
|
1702
2450
|
});
|
|
2451
|
+
var MARKER_COMPONENT_FIELDS = [
|
|
2452
|
+
"skills",
|
|
2453
|
+
"agents",
|
|
2454
|
+
"commands",
|
|
2455
|
+
"hooks",
|
|
2456
|
+
"mcpServers",
|
|
2457
|
+
"outputStyles",
|
|
2458
|
+
"themes",
|
|
2459
|
+
"monitors"
|
|
2460
|
+
];
|
|
1703
2461
|
|
|
1704
2462
|
// src/detector/markerFile.ts
|
|
2463
|
+
function hasComponentCuration(marker) {
|
|
2464
|
+
return MARKER_COMPONENT_FIELDS.some((key) => marker[key] !== undefined);
|
|
2465
|
+
}
|
|
2466
|
+
function markerSuppressesSplit(marker) {
|
|
2467
|
+
return marker.groups === undefined || marker.groups.length === 0;
|
|
2468
|
+
}
|
|
2469
|
+
function isFreezeOnlyMarker(marker) {
|
|
2470
|
+
return marker.groups !== undefined && !hasComponentCuration(marker);
|
|
2471
|
+
}
|
|
1705
2472
|
function detectMarkerFile(repoRoot) {
|
|
1706
|
-
const markerPath =
|
|
2473
|
+
const markerPath = join4(repoRoot, ".ccpluginizer.json");
|
|
1707
2474
|
if (!existsSync2(markerPath)) {
|
|
1708
2475
|
return null;
|
|
1709
2476
|
}
|
|
1710
|
-
const raw = readFileSync(markerPath, "utf8");
|
|
1711
2477
|
let parsed;
|
|
1712
2478
|
try {
|
|
1713
|
-
parsed =
|
|
1714
|
-
} catch (
|
|
1715
|
-
throw new MarkerFileError(
|
|
2479
|
+
parsed = readJsonFile(markerPath);
|
|
2480
|
+
} catch (e3) {
|
|
2481
|
+
throw new MarkerFileError(e3 instanceof Error ? e3.message : String(e3), []);
|
|
1716
2482
|
}
|
|
1717
2483
|
const result = safeParse(MarkerFileSchema, parsed);
|
|
1718
2484
|
if (!result.success) {
|
|
@@ -1722,35 +2488,40 @@ function detectMarkerFile(repoRoot) {
|
|
|
1722
2488
|
}
|
|
1723
2489
|
|
|
1724
2490
|
// src/detector/conventions.ts
|
|
1725
|
-
import {
|
|
1726
|
-
|
|
2491
|
+
import { join as join5 } from "node:path";
|
|
2492
|
+
var ARTIFACT_DIR_FOLDERS = ["commands", "output-styles", "themes"];
|
|
2493
|
+
var ARTIFACT_JSON_KINDS = ["hooks", "monitors"];
|
|
2494
|
+
var ARTIFACT_FOLDER_KIND = {
|
|
2495
|
+
commands: "commands",
|
|
2496
|
+
"output-styles": "outputStyles",
|
|
2497
|
+
themes: "themes"
|
|
2498
|
+
};
|
|
1727
2499
|
var FOLDER_KINDS = [
|
|
1728
2500
|
{ folder: "skills", kind: "skills", emit: "directory" },
|
|
1729
2501
|
{ folder: "agents", kind: "agents", emit: { enumerateFiles: ".md" } },
|
|
1730
|
-
{ folder
|
|
1731
|
-
{ folder: "output-styles", kind: "outputStyles", emit: "directory" },
|
|
1732
|
-
{ folder: "themes", kind: "themes", emit: "directory" }
|
|
2502
|
+
...ARTIFACT_DIR_FOLDERS.map((folder) => ({ folder, kind: ARTIFACT_FOLDER_KIND[folder], emit: "directory" }))
|
|
1733
2503
|
];
|
|
1734
2504
|
var FILE_KINDS = [
|
|
1735
2505
|
{ file: "hooks/hooks.json", kind: "hooks" },
|
|
1736
2506
|
{ file: ".mcp.json", kind: "mcpServers" },
|
|
1737
2507
|
{ file: "monitors/monitors.json", kind: "monitors" }
|
|
1738
2508
|
];
|
|
1739
|
-
function detectConventions(repoRoot) {
|
|
1740
|
-
const
|
|
1741
|
-
const
|
|
2509
|
+
function detectConventions(repoRoot, caches = {}) {
|
|
2510
|
+
const list = caches.list ?? makeDirLister();
|
|
2511
|
+
const rootFindings = scanRoot(repoRoot, "", list);
|
|
2512
|
+
const dotfilesFindings = scanRoot(repoRoot, ".claude", list);
|
|
1742
2513
|
return mergeByKind([...rootFindings, ...dotfilesFindings]);
|
|
1743
2514
|
}
|
|
1744
|
-
function scanRoot(repoRoot, prefix) {
|
|
2515
|
+
function scanRoot(repoRoot, prefix, list) {
|
|
1745
2516
|
const findings = [];
|
|
1746
|
-
const baseDir = prefix === "" ? repoRoot :
|
|
2517
|
+
const baseDir = prefix === "" ? repoRoot : join5(repoRoot, prefix);
|
|
1747
2518
|
const pathPrefix = prefix === "" ? "./" : `./${prefix}/`;
|
|
1748
2519
|
for (const { folder, kind, emit } of FOLDER_KINDS) {
|
|
1749
|
-
|
|
1750
|
-
if (!existsSync3(folderPath) || !statSync(folderPath).isDirectory()) {
|
|
2520
|
+
if (!dirContainsDir(list, baseDir, folder)) {
|
|
1751
2521
|
continue;
|
|
1752
2522
|
}
|
|
1753
|
-
const
|
|
2523
|
+
const folderPath = join5(baseDir, folder);
|
|
2524
|
+
const entries = list(folderPath).map((e3) => e3.name);
|
|
1754
2525
|
if (emit === "directory") {
|
|
1755
2526
|
findings.push({
|
|
1756
2527
|
kind,
|
|
@@ -1759,11 +2530,11 @@ function scanRoot(repoRoot, prefix) {
|
|
|
1759
2530
|
source: "convention"
|
|
1760
2531
|
});
|
|
1761
2532
|
} else {
|
|
1762
|
-
const files = entries.filter((
|
|
2533
|
+
const files = entries.filter((e3) => e3.endsWith(emit.enumerateFiles));
|
|
1763
2534
|
if (files.length > 0) {
|
|
1764
2535
|
findings.push({
|
|
1765
2536
|
kind,
|
|
1766
|
-
paths: files.map((
|
|
2537
|
+
paths: files.map((f2) => `${pathPrefix}${folder}/${f2}`),
|
|
1767
2538
|
confidence: "high",
|
|
1768
2539
|
source: "convention"
|
|
1769
2540
|
});
|
|
@@ -1771,7 +2542,10 @@ function scanRoot(repoRoot, prefix) {
|
|
|
1771
2542
|
}
|
|
1772
2543
|
}
|
|
1773
2544
|
for (const { file, kind } of FILE_KINDS) {
|
|
1774
|
-
|
|
2545
|
+
const parts = file.split("/");
|
|
2546
|
+
const fileName = parts[parts.length - 1] ?? file;
|
|
2547
|
+
const fileDir = parts.length > 1 ? join5(baseDir, ...parts.slice(0, -1)) : baseDir;
|
|
2548
|
+
if (list(fileDir).some((e3) => e3.isFile && e3.name === fileName)) {
|
|
1775
2549
|
findings.push({
|
|
1776
2550
|
kind,
|
|
1777
2551
|
paths: [`${pathPrefix}${file}`],
|
|
@@ -1803,8 +2577,8 @@ function mergeByKind(findings) {
|
|
|
1803
2577
|
}
|
|
1804
2578
|
|
|
1805
2579
|
// src/detector/nonStandardManifest.ts
|
|
1806
|
-
import { existsSync as
|
|
1807
|
-
import { join as
|
|
2580
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
2581
|
+
import { join as join6 } from "node:path";
|
|
1808
2582
|
|
|
1809
2583
|
// src/schemas/nonStandardManifest.ts
|
|
1810
2584
|
var NonStandardManifestSchema = object({
|
|
@@ -1824,8 +2598,8 @@ var NonStandardManifestSchema = object({
|
|
|
1824
2598
|
|
|
1825
2599
|
// src/detector/nonStandardManifest.ts
|
|
1826
2600
|
function detectNonStandardManifest(repoRoot) {
|
|
1827
|
-
const dir =
|
|
1828
|
-
if (!
|
|
2601
|
+
const dir = join6(repoRoot, ".claude-plugin");
|
|
2602
|
+
if (!existsSync3(dir) || !statSync2(dir).isDirectory()) {
|
|
1829
2603
|
return null;
|
|
1830
2604
|
}
|
|
1831
2605
|
for (const entry of readdirSync2(dir)) {
|
|
@@ -1835,7 +2609,7 @@ function detectNonStandardManifest(repoRoot) {
|
|
|
1835
2609
|
if (!entry.endsWith(".json")) {
|
|
1836
2610
|
continue;
|
|
1837
2611
|
}
|
|
1838
|
-
const filePath =
|
|
2612
|
+
const filePath = join6(dir, entry);
|
|
1839
2613
|
let parsed;
|
|
1840
2614
|
try {
|
|
1841
2615
|
parsed = JSON.parse(readFileSync2(filePath, "utf8"));
|
|
@@ -1846,119 +2620,263 @@ function detectNonStandardManifest(repoRoot) {
|
|
|
1846
2620
|
if (!result.success) {
|
|
1847
2621
|
continue;
|
|
1848
2622
|
}
|
|
1849
|
-
const
|
|
1850
|
-
const hasComponent =
|
|
2623
|
+
const m2 = result.output;
|
|
2624
|
+
const hasComponent = m2.skills !== undefined || m2.agents !== undefined || m2.commands !== undefined || m2.hooks !== undefined || m2.mcpServers !== undefined;
|
|
1851
2625
|
if (!hasComponent) {
|
|
1852
2626
|
continue;
|
|
1853
2627
|
}
|
|
1854
|
-
return { filename: entry, manifest:
|
|
2628
|
+
return { filename: entry, manifest: m2 };
|
|
1855
2629
|
}
|
|
1856
2630
|
return null;
|
|
1857
2631
|
}
|
|
1858
2632
|
|
|
1859
2633
|
// src/detector/contentSniff.ts
|
|
1860
|
-
import {
|
|
1861
|
-
|
|
2634
|
+
import { dirname, relative } from "node:path";
|
|
2635
|
+
|
|
2636
|
+
// src/detector/frontmatterIo.ts
|
|
2637
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1862
2638
|
|
|
1863
2639
|
// src/schemas/frontmatter.ts
|
|
2640
|
+
var isScalar = (val) => typeof val === "string" || typeof val === "number" || typeof val === "boolean";
|
|
2641
|
+
var Stringy = pipe(custom((val) => isScalar(val) || Array.isArray(val) && val.length > 0 && val.every(isScalar)), transform((val) => Array.isArray(val) ? val.map(String).join(", ") : String(val)));
|
|
2642
|
+
var AdvisoryString = pipe(unknown(), transform((val) => isScalar(val) ? String(val) : undefined));
|
|
2643
|
+
var AdvisoryMetadata = pipe(unknown(), transform((val) => val !== null && typeof val === "object" && !Array.isArray(val) ? val : {}), object({
|
|
2644
|
+
product: optional(AdvisoryString)
|
|
2645
|
+
}));
|
|
1864
2646
|
var SkillFrontmatterSchema = object({
|
|
1865
|
-
name: optional(
|
|
1866
|
-
description:
|
|
1867
|
-
"disable-model-invocation": optional(boolean())
|
|
2647
|
+
name: optional(Stringy),
|
|
2648
|
+
description: Stringy,
|
|
2649
|
+
"disable-model-invocation": optional(boolean()),
|
|
2650
|
+
metadata: optional(AdvisoryMetadata)
|
|
1868
2651
|
});
|
|
1869
2652
|
var AgentFrontmatterSchema = object({
|
|
1870
|
-
name:
|
|
1871
|
-
description:
|
|
2653
|
+
name: Stringy,
|
|
2654
|
+
description: Stringy,
|
|
1872
2655
|
model: optional(string()),
|
|
1873
2656
|
effort: optional(string()),
|
|
1874
2657
|
maxTurns: optional(number()),
|
|
1875
|
-
tools: optional(array(string())),
|
|
2658
|
+
tools: optional(union([string(), array(string())])),
|
|
1876
2659
|
disallowedTools: optional(union([string(), array(string())]))
|
|
1877
2660
|
});
|
|
1878
2661
|
|
|
1879
|
-
// src/detector/
|
|
1880
|
-
function
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
if (filePath.endsWith("SKILL.md")) {
|
|
1885
|
-
const fm = parseFrontmatter(filePath);
|
|
1886
|
-
if (fm !== null && safeParse(SkillFrontmatterSchema, fm).success) {
|
|
1887
|
-
const dir = dirname(filePath);
|
|
1888
|
-
skillDirs.add(dir);
|
|
1889
|
-
}
|
|
1890
|
-
return;
|
|
1891
|
-
}
|
|
1892
|
-
if (filePath.endsWith(".md")) {
|
|
1893
|
-
const fm = parseFrontmatter(filePath);
|
|
1894
|
-
if (fm !== null && safeParse(AgentFrontmatterSchema, fm).success) {
|
|
1895
|
-
agentFiles.add(filePath);
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
});
|
|
1899
|
-
const findings = [];
|
|
1900
|
-
if (skillDirs.size > 0) {
|
|
1901
|
-
findings.push({
|
|
1902
|
-
kind: "skills",
|
|
1903
|
-
paths: Array.from(skillDirs).map((dir) => `./${relative(repoRoot, dir)}/`),
|
|
1904
|
-
confidence: "medium",
|
|
1905
|
-
source: "sniff"
|
|
1906
|
-
});
|
|
1907
|
-
}
|
|
1908
|
-
if (agentFiles.size > 0) {
|
|
1909
|
-
findings.push({
|
|
1910
|
-
kind: "agents",
|
|
1911
|
-
paths: Array.from(agentFiles).map((file) => `./${relative(repoRoot, file)}`),
|
|
1912
|
-
confidence: "medium",
|
|
1913
|
-
source: "sniff"
|
|
1914
|
-
});
|
|
1915
|
-
}
|
|
1916
|
-
return findings;
|
|
1917
|
-
}
|
|
1918
|
-
function walk(repoRoot, dir, visit) {
|
|
1919
|
-
for (const entry of readdirSync3(dir)) {
|
|
1920
|
-
if (entry === "node_modules" || entry === ".git") {
|
|
1921
|
-
continue;
|
|
1922
|
-
}
|
|
1923
|
-
const fullPath = join6(dir, entry);
|
|
1924
|
-
const s = statSync3(fullPath);
|
|
1925
|
-
if (s.isDirectory()) {
|
|
1926
|
-
walk(repoRoot, fullPath, visit);
|
|
1927
|
-
} else if (s.isFile()) {
|
|
1928
|
-
visit(fullPath);
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
function parseFrontmatter(filePath) {
|
|
1933
|
-
const content = readFileSync3(filePath, "utf8");
|
|
1934
|
-
const match = /^---\n([\s\S]*?)\n---/.exec(content);
|
|
2662
|
+
// src/detector/yaml.ts
|
|
2663
|
+
function extractFrontmatter(fileContent) {
|
|
2664
|
+
const text = fileContent.replace(/^\uFEFF/, "").replace(/\r\n?/g, `
|
|
2665
|
+
`);
|
|
2666
|
+
const match = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
1935
2667
|
if (match === null) {
|
|
1936
2668
|
return null;
|
|
1937
2669
|
}
|
|
1938
|
-
const
|
|
1939
|
-
if (
|
|
2670
|
+
const body = match[1];
|
|
2671
|
+
if (body === undefined) {
|
|
1940
2672
|
return null;
|
|
1941
2673
|
}
|
|
1942
|
-
return
|
|
2674
|
+
return parseYamlFrontmatter(body);
|
|
1943
2675
|
}
|
|
1944
|
-
function
|
|
1945
|
-
const
|
|
1946
|
-
|
|
1947
|
-
`))
|
|
2676
|
+
function parseYamlFrontmatter(body) {
|
|
2677
|
+
const lines = dedentCommonIndent(body.replace(/\r\n?/g, `
|
|
2678
|
+
`).split(`
|
|
2679
|
+
`));
|
|
2680
|
+
const out = Object.create(null);
|
|
2681
|
+
let i3 = 0;
|
|
2682
|
+
while (i3 < lines.length) {
|
|
2683
|
+
const line = lines[i3] ?? "";
|
|
1948
2684
|
const trimmed = line.trim();
|
|
1949
2685
|
if (trimmed === "" || trimmed.startsWith("#")) {
|
|
2686
|
+
i3++;
|
|
2687
|
+
continue;
|
|
2688
|
+
}
|
|
2689
|
+
if (leadingSpaces(line) > 0) {
|
|
2690
|
+
i3++;
|
|
1950
2691
|
continue;
|
|
1951
2692
|
}
|
|
1952
2693
|
const colonIdx = trimmed.indexOf(":");
|
|
1953
2694
|
if (colonIdx === -1) {
|
|
2695
|
+
i3++;
|
|
1954
2696
|
continue;
|
|
1955
2697
|
}
|
|
1956
2698
|
const key = trimmed.slice(0, colonIdx).trim();
|
|
1957
2699
|
const rawValue = trimmed.slice(colonIdx + 1).trim();
|
|
1958
|
-
|
|
2700
|
+
if (isBlockScalarIndicator(rawValue)) {
|
|
2701
|
+
const { value, next } = collectBlockScalar(lines, i3 + 1, 0, rawValue);
|
|
2702
|
+
out[key] = value;
|
|
2703
|
+
i3 = next;
|
|
2704
|
+
continue;
|
|
2705
|
+
}
|
|
2706
|
+
if (rawValue === "") {
|
|
2707
|
+
const { value, next } = collectNested(lines, i3 + 1, 0);
|
|
2708
|
+
out[key] = value;
|
|
2709
|
+
i3 = next;
|
|
2710
|
+
continue;
|
|
2711
|
+
}
|
|
2712
|
+
const cont = collectPlainContinuation(lines, i3 + 1);
|
|
2713
|
+
if (cont.lines.length > 0) {
|
|
2714
|
+
out[key] = [rawValue, ...cont.lines].join(" ");
|
|
2715
|
+
i3 = cont.next;
|
|
2716
|
+
} else {
|
|
2717
|
+
out[key] = coerceYamlValue(rawValue);
|
|
2718
|
+
i3++;
|
|
2719
|
+
}
|
|
1959
2720
|
}
|
|
1960
2721
|
return out;
|
|
1961
2722
|
}
|
|
2723
|
+
function collectPlainContinuation(lines, start) {
|
|
2724
|
+
const collected = [];
|
|
2725
|
+
let i3 = start;
|
|
2726
|
+
for (;i3 < lines.length; i3++) {
|
|
2727
|
+
const line = lines[i3] ?? "";
|
|
2728
|
+
if (line.trim() === "" || line.trim().startsWith("#") || leadingSpaces(line) === 0) {
|
|
2729
|
+
break;
|
|
2730
|
+
}
|
|
2731
|
+
collected.push(line.trim());
|
|
2732
|
+
}
|
|
2733
|
+
return { lines: collected, next: i3 };
|
|
2734
|
+
}
|
|
2735
|
+
function dedentCommonIndent(lines) {
|
|
2736
|
+
let minIndent = Infinity;
|
|
2737
|
+
for (const line of lines) {
|
|
2738
|
+
const t2 = line.trim();
|
|
2739
|
+
if (t2 === "" || t2.startsWith("#")) {
|
|
2740
|
+
continue;
|
|
2741
|
+
}
|
|
2742
|
+
minIndent = Math.min(minIndent, leadingSpaces(line));
|
|
2743
|
+
}
|
|
2744
|
+
if (minIndent === 0 || !Number.isFinite(minIndent)) {
|
|
2745
|
+
return lines;
|
|
2746
|
+
}
|
|
2747
|
+
return lines.map((line) => line.trim() === "" ? line : line.slice(minIndent));
|
|
2748
|
+
}
|
|
2749
|
+
function leadingSpaces(line) {
|
|
2750
|
+
let n2 = 0;
|
|
2751
|
+
for (const ch of line) {
|
|
2752
|
+
if (ch === " " || ch === "\t") {
|
|
2753
|
+
n2++;
|
|
2754
|
+
} else {
|
|
2755
|
+
break;
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2758
|
+
return n2;
|
|
2759
|
+
}
|
|
2760
|
+
function isBlockScalarIndicator(raw) {
|
|
2761
|
+
return raw === ">" || raw === ">-" || raw === ">+" || raw === "|" || raw === "|-" || raw === "|+";
|
|
2762
|
+
}
|
|
2763
|
+
function collectBlockScalar(lines, start, baseIndent, indicator) {
|
|
2764
|
+
const folded = indicator.startsWith(">");
|
|
2765
|
+
const content = [];
|
|
2766
|
+
let blockIndent = -1;
|
|
2767
|
+
let i3 = start;
|
|
2768
|
+
for (;i3 < lines.length; i3++) {
|
|
2769
|
+
const line = lines[i3] ?? "";
|
|
2770
|
+
if (line.trim() === "") {
|
|
2771
|
+
content.push("");
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2774
|
+
if (leadingSpaces(line) <= baseIndent) {
|
|
2775
|
+
break;
|
|
2776
|
+
}
|
|
2777
|
+
if (blockIndent === -1) {
|
|
2778
|
+
blockIndent = leadingSpaces(line);
|
|
2779
|
+
}
|
|
2780
|
+
content.push(line.slice(Math.min(blockIndent, leadingSpaces(line))));
|
|
2781
|
+
}
|
|
2782
|
+
while (content.length > 0 && content[content.length - 1] === "") {
|
|
2783
|
+
content.pop();
|
|
2784
|
+
}
|
|
2785
|
+
if (!folded) {
|
|
2786
|
+
return { value: content.join(`
|
|
2787
|
+
`), next: i3 };
|
|
2788
|
+
}
|
|
2789
|
+
const paragraphs = [];
|
|
2790
|
+
let current = [];
|
|
2791
|
+
for (const line of content) {
|
|
2792
|
+
if (line === "") {
|
|
2793
|
+
paragraphs.push(current.join(" "));
|
|
2794
|
+
current = [];
|
|
2795
|
+
} else {
|
|
2796
|
+
current.push(line.trim());
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
paragraphs.push(current.join(" "));
|
|
2800
|
+
return { value: paragraphs.join(`
|
|
2801
|
+
`).trim(), next: i3 };
|
|
2802
|
+
}
|
|
2803
|
+
function collectNested(lines, start, baseIndent) {
|
|
2804
|
+
let peek = start;
|
|
2805
|
+
while (peek < lines.length) {
|
|
2806
|
+
const t2 = (lines[peek] ?? "").trim();
|
|
2807
|
+
if (t2 !== "" && !t2.startsWith("#")) {
|
|
2808
|
+
break;
|
|
2809
|
+
}
|
|
2810
|
+
peek++;
|
|
2811
|
+
}
|
|
2812
|
+
if (peek >= lines.length) {
|
|
2813
|
+
return { value: "", next: start };
|
|
2814
|
+
}
|
|
2815
|
+
const firstLine = lines[peek] ?? "";
|
|
2816
|
+
if (leadingSpaces(firstLine) <= baseIndent) {
|
|
2817
|
+
return { value: "", next: start };
|
|
2818
|
+
}
|
|
2819
|
+
const firstTrimmed = firstLine.trim();
|
|
2820
|
+
const childIndent = leadingSpaces(firstLine);
|
|
2821
|
+
if (firstTrimmed !== "-" && !firstTrimmed.startsWith("- ") && mappingColonIndex(firstTrimmed) === -1) {
|
|
2822
|
+
const cont = collectPlainContinuation(lines, peek);
|
|
2823
|
+
return { value: cont.lines.join(" "), next: cont.next };
|
|
2824
|
+
}
|
|
2825
|
+
if (firstTrimmed === "-" || firstTrimmed.startsWith("- ")) {
|
|
2826
|
+
const arr = [];
|
|
2827
|
+
let i4 = start;
|
|
2828
|
+
for (;i4 < lines.length; i4++) {
|
|
2829
|
+
const line = lines[i4] ?? "";
|
|
2830
|
+
if (line.trim() === "") {
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
if (leadingSpaces(line) <= baseIndent) {
|
|
2834
|
+
break;
|
|
2835
|
+
}
|
|
2836
|
+
if (leadingSpaces(line) > childIndent) {
|
|
2837
|
+
continue;
|
|
2838
|
+
}
|
|
2839
|
+
const t2 = line.trim();
|
|
2840
|
+
if (t2 === "-") {
|
|
2841
|
+
arr.push(null);
|
|
2842
|
+
} else if (t2.startsWith("- ")) {
|
|
2843
|
+
arr.push(coerceYamlValue(t2.slice(2).trim()));
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
return { value: arr, next: i4 };
|
|
2847
|
+
}
|
|
2848
|
+
const map = Object.create(null);
|
|
2849
|
+
let i3 = start;
|
|
2850
|
+
for (;i3 < lines.length; i3++) {
|
|
2851
|
+
const line = lines[i3] ?? "";
|
|
2852
|
+
const t2 = line.trim();
|
|
2853
|
+
if (t2 === "" || t2.startsWith("#")) {
|
|
2854
|
+
continue;
|
|
2855
|
+
}
|
|
2856
|
+
if (leadingSpaces(line) <= baseIndent) {
|
|
2857
|
+
break;
|
|
2858
|
+
}
|
|
2859
|
+
if (leadingSpaces(line) > childIndent) {
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
const ci = mappingColonIndex(t2);
|
|
2863
|
+
if (ci === -1) {
|
|
2864
|
+
continue;
|
|
2865
|
+
}
|
|
2866
|
+
const k3 = t2.slice(0, ci).trim();
|
|
2867
|
+
const val = t2.slice(ci + 1).trim();
|
|
2868
|
+
map[k3] = val === "" || isBlockScalarIndicator(val) ? null : coerceYamlValue(val);
|
|
2869
|
+
}
|
|
2870
|
+
return { value: map, next: i3 };
|
|
2871
|
+
}
|
|
2872
|
+
function mappingColonIndex(line) {
|
|
2873
|
+
for (let i3 = line.indexOf(":");i3 !== -1; i3 = line.indexOf(":", i3 + 1)) {
|
|
2874
|
+
if (i3 === line.length - 1 || line[i3 + 1] === " " || line[i3 + 1] === "\t") {
|
|
2875
|
+
return i3;
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
return -1;
|
|
2879
|
+
}
|
|
1962
2880
|
function coerceYamlValue(raw) {
|
|
1963
2881
|
if (raw === "true")
|
|
1964
2882
|
return true;
|
|
@@ -1966,17 +2884,144 @@ function coerceYamlValue(raw) {
|
|
|
1966
2884
|
return false;
|
|
1967
2885
|
if (raw === "null" || raw === "~")
|
|
1968
2886
|
return null;
|
|
1969
|
-
if (/^-?\d+$/.test(raw))
|
|
1970
|
-
|
|
2887
|
+
if (/^-?\d+$/.test(raw)) {
|
|
2888
|
+
const n2 = Number(raw);
|
|
2889
|
+
return String(n2) === raw ? n2 : raw;
|
|
2890
|
+
}
|
|
1971
2891
|
if (raw.startsWith('"') && raw.endsWith('"'))
|
|
1972
2892
|
return raw.slice(1, -1);
|
|
1973
2893
|
if (raw.startsWith("'") && raw.endsWith("'"))
|
|
1974
2894
|
return raw.slice(1, -1);
|
|
2895
|
+
if (raw.startsWith("[") && raw.endsWith("]")) {
|
|
2896
|
+
const inner = raw.slice(1, -1).trim();
|
|
2897
|
+
if (inner === "")
|
|
2898
|
+
return [];
|
|
2899
|
+
if (!inner.includes("[") && !inner.includes("]")) {
|
|
2900
|
+
return splitFlowList(inner).map((s3) => coerceYamlValue(s3.trim()));
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
1975
2903
|
return raw;
|
|
1976
2904
|
}
|
|
2905
|
+
function splitFlowList(inner) {
|
|
2906
|
+
const parts = [];
|
|
2907
|
+
let current = "";
|
|
2908
|
+
let quote = null;
|
|
2909
|
+
for (const ch of inner) {
|
|
2910
|
+
if (quote !== null) {
|
|
2911
|
+
if (ch === quote) {
|
|
2912
|
+
quote = null;
|
|
2913
|
+
}
|
|
2914
|
+
current += ch;
|
|
2915
|
+
} else if (ch === '"' || ch === "'") {
|
|
2916
|
+
quote = ch;
|
|
2917
|
+
current += ch;
|
|
2918
|
+
} else if (ch === ",") {
|
|
2919
|
+
parts.push(current);
|
|
2920
|
+
current = "";
|
|
2921
|
+
} else {
|
|
2922
|
+
current += ch;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
parts.push(current);
|
|
2926
|
+
return parts;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
// src/detector/frontmatterIo.ts
|
|
2930
|
+
function readFrontmatter(filePath, onSkip) {
|
|
2931
|
+
let raw;
|
|
2932
|
+
try {
|
|
2933
|
+
raw = readFileSync3(filePath, "utf8");
|
|
2934
|
+
} catch (err) {
|
|
2935
|
+
if (isPermissionError(err)) {
|
|
2936
|
+
onSkip?.(filePath, err);
|
|
2937
|
+
}
|
|
2938
|
+
return null;
|
|
2939
|
+
}
|
|
2940
|
+
return extractFrontmatter(raw);
|
|
2941
|
+
}
|
|
2942
|
+
function makeFrontmatterReader(onSkip) {
|
|
2943
|
+
const cache = new Map;
|
|
2944
|
+
return (file) => {
|
|
2945
|
+
if (cache.has(file)) {
|
|
2946
|
+
return cache.get(file) ?? null;
|
|
2947
|
+
}
|
|
2948
|
+
const fm = readFrontmatter(file, onSkip);
|
|
2949
|
+
cache.set(file, fm);
|
|
2950
|
+
return fm;
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
function isAgentFile(filePath, readFm = readFrontmatter) {
|
|
2954
|
+
const fm = readFm(filePath);
|
|
2955
|
+
return fm !== null && safeParse(AgentFrontmatterSchema, fm).success;
|
|
2956
|
+
}
|
|
2957
|
+
function parseSkillFile(filePath, readFm = readFrontmatter) {
|
|
2958
|
+
const fm = readFm(filePath);
|
|
2959
|
+
if (fm === null) {
|
|
2960
|
+
return null;
|
|
2961
|
+
}
|
|
2962
|
+
const parsed = safeParse(SkillFrontmatterSchema, fm);
|
|
2963
|
+
return parsed.success ? parsed.output : null;
|
|
2964
|
+
}
|
|
2965
|
+
function isSkillFile(filePath, readFm = readFrontmatter) {
|
|
2966
|
+
return parseSkillFile(filePath, readFm) !== null;
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
// src/detector/contentSniff.ts
|
|
2970
|
+
var SNIFF_SKIP_DIRS = new Set(["node_modules", ".git"]);
|
|
2971
|
+
function detectContentSniff(repoRoot, caches = {}) {
|
|
2972
|
+
const skillDirs = new Set;
|
|
2973
|
+
const agentFiles = new Set;
|
|
2974
|
+
const readFm = caches.readFrontmatter ?? readFrontmatter;
|
|
2975
|
+
walkTree(repoRoot, {
|
|
2976
|
+
skipDirs: SNIFF_SKIP_DIRS,
|
|
2977
|
+
...caches.list !== undefined ? { list: caches.list } : {},
|
|
2978
|
+
onFile: (filePath) => {
|
|
2979
|
+
if (filePath.endsWith("SKILL.md")) {
|
|
2980
|
+
if (isSkillFile(filePath, readFm)) {
|
|
2981
|
+
skillDirs.add(dirname(filePath));
|
|
2982
|
+
}
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
if (filePath.endsWith(".md") && isAgentFile(filePath, readFm)) {
|
|
2986
|
+
agentFiles.add(filePath);
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
});
|
|
2990
|
+
const findings = [];
|
|
2991
|
+
if (skillDirs.size > 0) {
|
|
2992
|
+
findings.push({
|
|
2993
|
+
kind: "skills",
|
|
2994
|
+
paths: Array.from(skillDirs).map((dir) => `./${relative(repoRoot, dir)}/`).sort(),
|
|
2995
|
+
confidence: "medium",
|
|
2996
|
+
source: "sniff"
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2999
|
+
if (agentFiles.size > 0) {
|
|
3000
|
+
findings.push({
|
|
3001
|
+
kind: "agents",
|
|
3002
|
+
paths: Array.from(agentFiles).map((file) => `./${relative(repoRoot, file)}`).sort(),
|
|
3003
|
+
confidence: "medium",
|
|
3004
|
+
source: "sniff"
|
|
3005
|
+
});
|
|
3006
|
+
}
|
|
3007
|
+
return findings;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// src/detector/caches.ts
|
|
3011
|
+
function makeScanCaches() {
|
|
3012
|
+
const skippedPaths = [];
|
|
3013
|
+
const onSkip = (p2) => {
|
|
3014
|
+
skippedPaths.push(p2);
|
|
3015
|
+
};
|
|
3016
|
+
return {
|
|
3017
|
+
list: makeDirLister(onSkip),
|
|
3018
|
+
readFrontmatter: makeFrontmatterReader(onSkip),
|
|
3019
|
+
skippedPaths
|
|
3020
|
+
};
|
|
3021
|
+
}
|
|
1977
3022
|
|
|
1978
3023
|
// src/detector/normalize.ts
|
|
1979
|
-
import { existsSync as
|
|
3024
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1980
3025
|
import { join as join7 } from "node:path";
|
|
1981
3026
|
function normalizePath(input) {
|
|
1982
3027
|
if (input.split("/").includes("..")) {
|
|
@@ -1999,7 +3044,7 @@ function normalizePathsAgainstRepo(repoRoot, paths) {
|
|
|
1999
3044
|
for (const raw of paths) {
|
|
2000
3045
|
const normalized = normalizePath(raw);
|
|
2001
3046
|
const fullPath = join7(repoRoot, normalized);
|
|
2002
|
-
if (
|
|
3047
|
+
if (existsSync4(fullPath)) {
|
|
2003
3048
|
kept.push(normalized);
|
|
2004
3049
|
} else {
|
|
2005
3050
|
dropped.push(normalized);
|
|
@@ -2008,23 +3053,838 @@ function normalizePathsAgainstRepo(repoRoot, paths) {
|
|
|
2008
3053
|
return { kept, dropped };
|
|
2009
3054
|
}
|
|
2010
3055
|
|
|
3056
|
+
// src/detector/sourceLayout.ts
|
|
3057
|
+
import { basename, join as join9, relative as relative2, resolve, sep } from "node:path";
|
|
3058
|
+
|
|
3059
|
+
// src/detector/slugify.ts
|
|
3060
|
+
var SLUG_FALLBACK = "group";
|
|
3061
|
+
function byCodeUnit(a3, b6) {
|
|
3062
|
+
return a3 < b6 ? -1 : a3 > b6 ? 1 : 0;
|
|
3063
|
+
}
|
|
3064
|
+
function slugify(input) {
|
|
3065
|
+
const slug = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
3066
|
+
return slug === "" ? SLUG_FALLBACK : slug;
|
|
3067
|
+
}
|
|
3068
|
+
function stripCommonPrefix(keys) {
|
|
3069
|
+
if (keys.length <= 1) {
|
|
3070
|
+
return [...keys];
|
|
3071
|
+
}
|
|
3072
|
+
let prefix = keys[0] ?? "";
|
|
3073
|
+
for (const k3 of keys.slice(1)) {
|
|
3074
|
+
while (prefix !== "" && !k3.startsWith(prefix)) {
|
|
3075
|
+
prefix = prefix.slice(0, -1);
|
|
3076
|
+
}
|
|
3077
|
+
if (prefix === "") {
|
|
3078
|
+
return [...keys];
|
|
3079
|
+
}
|
|
3080
|
+
}
|
|
3081
|
+
const lastHyphen = prefix.lastIndexOf("-");
|
|
3082
|
+
if (lastHyphen === -1) {
|
|
3083
|
+
return [...keys];
|
|
3084
|
+
}
|
|
3085
|
+
const cut = lastHyphen + 1;
|
|
3086
|
+
const stripped = keys.map((k3) => k3.slice(cut));
|
|
3087
|
+
if (stripped.some((s3) => s3 === "")) {
|
|
3088
|
+
return [...keys];
|
|
3089
|
+
}
|
|
3090
|
+
return stripped;
|
|
3091
|
+
}
|
|
3092
|
+
function uniqueWithin(used, desired) {
|
|
3093
|
+
if (!used.has(desired)) {
|
|
3094
|
+
return desired;
|
|
3095
|
+
}
|
|
3096
|
+
let n2 = 2;
|
|
3097
|
+
while (used.has(`${desired}-${String(n2)}`)) {
|
|
3098
|
+
n2++;
|
|
3099
|
+
}
|
|
3100
|
+
return `${desired}-${String(n2)}`;
|
|
3101
|
+
}
|
|
3102
|
+
function uniqueSlugs(slugs) {
|
|
3103
|
+
const used = new Set;
|
|
3104
|
+
const out = [];
|
|
3105
|
+
for (const s3 of slugs) {
|
|
3106
|
+
const candidate = uniqueWithin(used, s3);
|
|
3107
|
+
used.add(candidate);
|
|
3108
|
+
out.push(candidate);
|
|
3109
|
+
}
|
|
3110
|
+
return out;
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
// src/detector/skillMeta.ts
|
|
3114
|
+
import { join as join8 } from "node:path";
|
|
3115
|
+
function enumerateSkills(containerDir, list = makeDirLister(), readFm = readFrontmatter) {
|
|
3116
|
+
const out = [];
|
|
3117
|
+
const children = list(containerDir).filter((e3) => e3.isDirectory).map((e3) => e3.name).sort();
|
|
3118
|
+
for (const dir of children) {
|
|
3119
|
+
const skillPath = join8(containerDir, dir);
|
|
3120
|
+
if (!dirContainsFile(list, skillPath, "SKILL.md")) {
|
|
3121
|
+
continue;
|
|
3122
|
+
}
|
|
3123
|
+
const parsed = parseSkillFile(join8(skillPath, "SKILL.md"), readFm);
|
|
3124
|
+
if (parsed === null) {
|
|
3125
|
+
continue;
|
|
3126
|
+
}
|
|
3127
|
+
out.push(toMeta(dir, parsed));
|
|
3128
|
+
}
|
|
3129
|
+
return out;
|
|
3130
|
+
}
|
|
3131
|
+
function toMeta(dir, fm) {
|
|
3132
|
+
return {
|
|
3133
|
+
path: `./${dir}/`,
|
|
3134
|
+
dir,
|
|
3135
|
+
name: fm.name ?? dir,
|
|
3136
|
+
description: fm.description,
|
|
3137
|
+
...fm.metadata?.product !== undefined ? { product: fm.metadata.product } : {}
|
|
3138
|
+
};
|
|
3139
|
+
}
|
|
3140
|
+
function countSkillMdDirs(containerDir, list = makeDirLister(), skipDirs) {
|
|
3141
|
+
return list(containerDir).filter((e3) => e3.isDirectory && !(skipDirs?.has(e3.name) ?? false) && dirContainsFile(list, join8(containerDir, e3.name), "SKILL.md")).length;
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
// src/detector/sourceLayout.ts
|
|
3145
|
+
import { realpathSync } from "node:fs";
|
|
3146
|
+
var SKIP_DIRS = new Set([
|
|
3147
|
+
".git",
|
|
3148
|
+
"node_modules",
|
|
3149
|
+
"tests",
|
|
3150
|
+
"test",
|
|
3151
|
+
"__tests__",
|
|
3152
|
+
"__mocks__",
|
|
3153
|
+
"dist",
|
|
3154
|
+
"build",
|
|
3155
|
+
"out",
|
|
3156
|
+
"coverage",
|
|
3157
|
+
".next",
|
|
3158
|
+
".cache",
|
|
3159
|
+
".turbo",
|
|
3160
|
+
".github",
|
|
3161
|
+
"vendor"
|
|
3162
|
+
]);
|
|
3163
|
+
function createLayoutResolver(repoRoot, caches = {}) {
|
|
3164
|
+
repoRoot = resolve(repoRoot);
|
|
3165
|
+
const list = caches.list ?? makeDirLister();
|
|
3166
|
+
const readFm = caches.readFrontmatter ?? makeFrontmatterReader();
|
|
3167
|
+
const dirs = [];
|
|
3168
|
+
walkTree(repoRoot, { skipDirs: SKIP_DIRS, list, onDir: (d2) => dirs.push(d2) });
|
|
3169
|
+
const insideSkillMemo = new Map;
|
|
3170
|
+
const insideSkill = (dir) => {
|
|
3171
|
+
if (dir === repoRoot) {
|
|
3172
|
+
return false;
|
|
3173
|
+
}
|
|
3174
|
+
const memo = insideSkillMemo.get(dir);
|
|
3175
|
+
if (memo !== undefined) {
|
|
3176
|
+
return memo;
|
|
3177
|
+
}
|
|
3178
|
+
const parent = join9(dir, "..");
|
|
3179
|
+
let result;
|
|
3180
|
+
if (parent === dir || parent === repoRoot && (basename(dir) === "skills" || basename(dir) === "agents")) {
|
|
3181
|
+
result = false;
|
|
3182
|
+
} else {
|
|
3183
|
+
const parentBundles = !dirIsPluginRoot(list, parent) && dirContainsFile(list, parent, "SKILL.md");
|
|
3184
|
+
result = parentBundles || insideSkill(parent);
|
|
3185
|
+
}
|
|
3186
|
+
insideSkillMemo.set(dir, result);
|
|
3187
|
+
return result;
|
|
3188
|
+
};
|
|
3189
|
+
const containerDirs = dirs.filter((d2) => !insideSkill(d2) && (basename(d2) === "skills" || !dirContainsFile(list, d2, "SKILL.md")));
|
|
3190
|
+
const counted = containerDirs.map((absDir) => ({
|
|
3191
|
+
absDir,
|
|
3192
|
+
relPath: toRel(repoRoot, absDir),
|
|
3193
|
+
count: countSkillMdDirs(absDir, list, SKIP_DIRS)
|
|
3194
|
+
}));
|
|
3195
|
+
const best = pickBest(counted);
|
|
3196
|
+
const skillsContainer = best === null ? null : { absDir: best.absDir, relPath: best.relPath, hasSkillsChild: dirContainsDir(list, best.absDir, "skills") };
|
|
3197
|
+
const totalSkillDirs = counted.reduce((sum, c3) => sum + c3.count, 0);
|
|
3198
|
+
const skillDirsOutsideContainer = totalSkillDirs - (best?.count ?? 0);
|
|
3199
|
+
const containerWarnings = [];
|
|
3200
|
+
if (best !== null) {
|
|
3201
|
+
try {
|
|
3202
|
+
const realContainer = realpathSync(best.absDir);
|
|
3203
|
+
const expected = join9(realpathSync(repoRoot), ...best.relPath === "." ? [] : best.relPath.split("/"));
|
|
3204
|
+
if (realContainer !== expected) {
|
|
3205
|
+
containerWarnings.push(`the skills container "${best.relPath}" resolves through a symlink; git-subdir checkouts would contain the link, not the skills — restructure or use --no-split.`);
|
|
3206
|
+
}
|
|
3207
|
+
} catch {}
|
|
3208
|
+
}
|
|
3209
|
+
let full = null;
|
|
3210
|
+
return {
|
|
3211
|
+
skillsContainer,
|
|
3212
|
+
skillDirsOutsideContainer,
|
|
3213
|
+
warnings: containerWarnings,
|
|
3214
|
+
list,
|
|
3215
|
+
readFrontmatter: readFm,
|
|
3216
|
+
full() {
|
|
3217
|
+
if (full === null) {
|
|
3218
|
+
const pluginRoot = resolvePluginRoot(repoRoot, dirs, list);
|
|
3219
|
+
full = {
|
|
3220
|
+
skillsContainer,
|
|
3221
|
+
skillDirsOutsideContainer,
|
|
3222
|
+
agentsContainer: resolveAgentsContainer(repoRoot, containerDirs, list, readFm),
|
|
3223
|
+
pluginRoot,
|
|
3224
|
+
mcp: resolveMcp(repoRoot, dirs, list, pluginRoot),
|
|
3225
|
+
artifacts: resolveArtifacts(repoRoot, dirs, list, pluginRoot)
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
return full;
|
|
3229
|
+
}
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
function toRel(repoRoot, abs) {
|
|
3233
|
+
const r2 = relative2(repoRoot, abs);
|
|
3234
|
+
return r2 === "" ? "." : r2.split(sep).join("/");
|
|
3235
|
+
}
|
|
3236
|
+
function pickBest(candidates) {
|
|
3237
|
+
let best = null;
|
|
3238
|
+
for (const c3 of candidates) {
|
|
3239
|
+
if (c3.count === 0) {
|
|
3240
|
+
continue;
|
|
3241
|
+
}
|
|
3242
|
+
if (best === null || isBetter(c3, best)) {
|
|
3243
|
+
best = c3;
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
return best;
|
|
3247
|
+
}
|
|
3248
|
+
function isBetter(a3, b6) {
|
|
3249
|
+
return a3.count > b6.count || a3.count === b6.count && shallowerThenCodeUnit(a3.relPath, b6.relPath) < 0;
|
|
3250
|
+
}
|
|
3251
|
+
function shallowerThenCodeUnit(a3, b6) {
|
|
3252
|
+
const segA = a3.split("/").length;
|
|
3253
|
+
const segB = b6.split("/").length;
|
|
3254
|
+
return segA !== segB ? segA - segB : byCodeUnit(a3, b6);
|
|
3255
|
+
}
|
|
3256
|
+
function resolveAgentsContainer(repoRoot, dirs, list, readFm) {
|
|
3257
|
+
const score = (absDir) => {
|
|
3258
|
+
const files = list(absDir).filter((e3) => e3.isFile && e3.name.endsWith(".md") && e3.name !== "SKILL.md").map((e3) => e3.name).filter((f2) => isAgentFile(join9(absDir, f2), readFm)).sort();
|
|
3259
|
+
return {
|
|
3260
|
+
absDir,
|
|
3261
|
+
relPath: toRel(repoRoot, absDir),
|
|
3262
|
+
count: files.length,
|
|
3263
|
+
files,
|
|
3264
|
+
hasSkillsChild: dirContainsDir(list, absDir, "skills")
|
|
3265
|
+
};
|
|
3266
|
+
};
|
|
3267
|
+
const named = dirs.filter((d2) => basename(d2) === "agents").map(score);
|
|
3268
|
+
const best = pickBest(named) ?? pickBest(dirs.map(score));
|
|
3269
|
+
return best === null ? null : { absDir: best.absDir, relPath: best.relPath, hasSkillsChild: best.hasSkillsChild, files: best.files };
|
|
3270
|
+
}
|
|
3271
|
+
function dirIsPluginRoot(list, dir) {
|
|
3272
|
+
return dirContainsDir(list, dir, ".claude-plugin") && dirContainsFile(list, join9(dir, ".claude-plugin"), "plugin.json");
|
|
3273
|
+
}
|
|
3274
|
+
function resolvePluginRoot(repoRoot, dirs, list) {
|
|
3275
|
+
const candidates = dirs.filter((d2) => dirIsPluginRoot(list, d2)).map((d2) => ({ relPath: toRel(repoRoot, d2), count: 1 }));
|
|
3276
|
+
const best = pickBest(candidates);
|
|
3277
|
+
return best === null ? null : { relPath: best.relPath };
|
|
3278
|
+
}
|
|
3279
|
+
function anchoredDirs(repoRoot, dirs, pluginRoot) {
|
|
3280
|
+
const anchors = new Set([repoRoot, join9(repoRoot, ".claude")]);
|
|
3281
|
+
if (pluginRoot !== null && pluginRoot.relPath !== ".") {
|
|
3282
|
+
const root = join9(repoRoot, pluginRoot.relPath);
|
|
3283
|
+
anchors.add(root);
|
|
3284
|
+
anchors.add(join9(root, ".claude"));
|
|
3285
|
+
}
|
|
3286
|
+
return dirs.filter((d2) => anchors.has(d2));
|
|
3287
|
+
}
|
|
3288
|
+
function resolveMcp(repoRoot, dirs, list, pluginRoot) {
|
|
3289
|
+
const files = anchoredDirs(repoRoot, dirs, pluginRoot).filter((d2) => dirContainsFile(list, d2, ".mcp.json")).map((d2) => ({ file: join9(d2, ".mcp.json"), relPath: toRel(repoRoot, join9(d2, ".mcp.json")) })).sort((a3, b6) => preferUnder(pluginRoot, a3.relPath, b6.relPath));
|
|
3290
|
+
for (const chosen of files) {
|
|
3291
|
+
let parsed;
|
|
3292
|
+
try {
|
|
3293
|
+
parsed = readJsonFile(chosen.file);
|
|
3294
|
+
} catch {
|
|
3295
|
+
continue;
|
|
3296
|
+
}
|
|
3297
|
+
const servers = extractServers(parsed);
|
|
3298
|
+
if (servers === null) {
|
|
3299
|
+
continue;
|
|
3300
|
+
}
|
|
3301
|
+
return { servers, serverType: classifyServers(servers), relPath: chosen.relPath };
|
|
3302
|
+
}
|
|
3303
|
+
return null;
|
|
3304
|
+
}
|
|
3305
|
+
function resolveArtifacts(repoRoot, dirs, list, pluginRoot) {
|
|
3306
|
+
const out = [];
|
|
3307
|
+
const anchored = anchoredDirs(repoRoot, dirs, pluginRoot);
|
|
3308
|
+
for (const kind of ARTIFACT_JSON_KINDS) {
|
|
3309
|
+
const hit = resolveJsonFileArtifact(repoRoot, anchored, list, pluginRoot, kind);
|
|
3310
|
+
if (hit !== null) {
|
|
3311
|
+
out.push({ kind, relPath: hit });
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
for (const kind of ARTIFACT_DIR_FOLDERS) {
|
|
3315
|
+
const hits = anchored.filter((d2) => dirContainsDir(list, d2, kind)).map((d2) => toRel(repoRoot, join9(d2, kind))).sort((a3, b6) => preferUnder(pluginRoot, a3, b6));
|
|
3316
|
+
if (hits[0] !== undefined) {
|
|
3317
|
+
out.push({ kind, relPath: hits[0] });
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
return out;
|
|
3321
|
+
}
|
|
3322
|
+
function resolveJsonFileArtifact(repoRoot, dirs, list, pluginRoot, kind) {
|
|
3323
|
+
const fileName = `${kind}.json`;
|
|
3324
|
+
const hits = dirs.filter((d2) => dirContainsDir(list, d2, kind) && dirContainsFile(list, join9(d2, kind), fileName)).map((d2) => toRel(repoRoot, join9(d2, kind, fileName))).sort((a3, b6) => preferUnder(pluginRoot, a3, b6));
|
|
3325
|
+
return hits[0] ?? null;
|
|
3326
|
+
}
|
|
3327
|
+
function preferUnder(pluginRoot, a3, b6) {
|
|
3328
|
+
if (pluginRoot !== null) {
|
|
3329
|
+
const aUnder = a3.startsWith(`${pluginRoot.relPath}/`) ? 0 : 1;
|
|
3330
|
+
const bUnder = b6.startsWith(`${pluginRoot.relPath}/`) ? 0 : 1;
|
|
3331
|
+
if (aUnder !== bUnder) {
|
|
3332
|
+
return aUnder - bUnder;
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
return shallowerThenCodeUnit(a3, b6);
|
|
3336
|
+
}
|
|
3337
|
+
function extractServers(parsed) {
|
|
3338
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3339
|
+
return null;
|
|
3340
|
+
}
|
|
3341
|
+
const obj = parsed;
|
|
3342
|
+
const hasWrapper = obj["mcpServers"] !== undefined && obj["mcpServers"] !== null;
|
|
3343
|
+
const raw = hasWrapper ? obj["mcpServers"] : obj;
|
|
3344
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
3345
|
+
return null;
|
|
3346
|
+
}
|
|
3347
|
+
const servers = raw;
|
|
3348
|
+
const entries = Object.values(servers);
|
|
3349
|
+
if (entries.length === 0) {
|
|
3350
|
+
return null;
|
|
3351
|
+
}
|
|
3352
|
+
if (!hasWrapper && !entries.every((e3) => e3 !== null && typeof e3 === "object")) {
|
|
3353
|
+
return null;
|
|
3354
|
+
}
|
|
3355
|
+
return servers;
|
|
3356
|
+
}
|
|
3357
|
+
function classifyServers(servers) {
|
|
3358
|
+
const types = Object.values(servers).map(classifyOne);
|
|
3359
|
+
if (types.length === 0) {
|
|
3360
|
+
return "unknown";
|
|
3361
|
+
}
|
|
3362
|
+
if (types.includes("repo-local")) {
|
|
3363
|
+
return "repo-local";
|
|
3364
|
+
}
|
|
3365
|
+
if (types.every((t2) => t2 === "remote")) {
|
|
3366
|
+
return "remote";
|
|
3367
|
+
}
|
|
3368
|
+
if (types.every((t2) => t2 === "remote" || t2 === "package")) {
|
|
3369
|
+
return "package";
|
|
3370
|
+
}
|
|
3371
|
+
return "unknown";
|
|
3372
|
+
}
|
|
3373
|
+
var SCRIPT_EXT = /\.(?:js|mjs|cjs|ts|mts|cts|py|sh|rb)$/;
|
|
3374
|
+
function classifyOne(server) {
|
|
3375
|
+
if (server === null || typeof server !== "object") {
|
|
3376
|
+
return "unknown";
|
|
3377
|
+
}
|
|
3378
|
+
const s3 = server;
|
|
3379
|
+
if (s3["type"] === "http" || s3["type"] === "sse" || typeof s3["url"] === "string") {
|
|
3380
|
+
return "remote";
|
|
3381
|
+
}
|
|
3382
|
+
const cmd = s3["command"];
|
|
3383
|
+
if (typeof cmd !== "string") {
|
|
3384
|
+
return "unknown";
|
|
3385
|
+
}
|
|
3386
|
+
const args = Array.isArray(s3["args"]) ? s3["args"].filter((a3) => typeof a3 === "string") : [];
|
|
3387
|
+
const refsLocal = (x4) => x4.startsWith("./") || x4.startsWith("../") || x4.startsWith("/") || x4.includes("${CLAUDE_PLUGIN_ROOT}") || x4.includes("/") && SCRIPT_EXT.test(x4);
|
|
3388
|
+
if (refsLocal(cmd) || args.some(refsLocal)) {
|
|
3389
|
+
return "repo-local";
|
|
3390
|
+
}
|
|
3391
|
+
return "package";
|
|
3392
|
+
}
|
|
3393
|
+
|
|
3394
|
+
// src/detector/partition.ts
|
|
3395
|
+
import { basename as basename2 } from "node:path";
|
|
3396
|
+
|
|
3397
|
+
// src/schemas/rawGroups.ts
|
|
3398
|
+
var RawGroupSchema = object({
|
|
3399
|
+
slug: string(),
|
|
3400
|
+
members: array(string())
|
|
3401
|
+
});
|
|
3402
|
+
var RawGroupsSchema = array(RawGroupSchema);
|
|
3403
|
+
|
|
3404
|
+
// src/detector/partition.ts
|
|
3405
|
+
var CLUSTER_STRATEGIES = ["auto", "auto-llm", "llm", "metadata", "directory", "name-prefix"];
|
|
3406
|
+
var DEFAULT_STRATEGY = "auto";
|
|
3407
|
+
function strategyUsesLlm(strategy) {
|
|
3408
|
+
return strategy === "llm" || strategy === "auto-llm";
|
|
3409
|
+
}
|
|
3410
|
+
var MIN_K = 2;
|
|
3411
|
+
var MAX_K = 12;
|
|
3412
|
+
var MAX_FRACTION = 0.7;
|
|
3413
|
+
var MIN_GROUP_SIZE = 3;
|
|
3414
|
+
var MISC = "misc";
|
|
3415
|
+
var LANGUAGES = new Set([
|
|
3416
|
+
"curl",
|
|
3417
|
+
"go",
|
|
3418
|
+
"golang",
|
|
3419
|
+
"java",
|
|
3420
|
+
"javascript",
|
|
3421
|
+
"js",
|
|
3422
|
+
"node",
|
|
3423
|
+
"nodejs",
|
|
3424
|
+
"typescript",
|
|
3425
|
+
"ts",
|
|
3426
|
+
"python",
|
|
3427
|
+
"py",
|
|
3428
|
+
"ruby",
|
|
3429
|
+
"rb",
|
|
3430
|
+
"php",
|
|
3431
|
+
"csharp",
|
|
3432
|
+
"dotnet",
|
|
3433
|
+
"cs",
|
|
3434
|
+
"rust",
|
|
3435
|
+
"rs",
|
|
3436
|
+
"kotlin",
|
|
3437
|
+
"swift",
|
|
3438
|
+
"bash",
|
|
3439
|
+
"shell",
|
|
3440
|
+
"sh",
|
|
3441
|
+
"cpp",
|
|
3442
|
+
"c"
|
|
3443
|
+
]);
|
|
3444
|
+
function partitionByMetadata(skills) {
|
|
3445
|
+
if (!skills.some((s3) => s3.product !== undefined)) {
|
|
3446
|
+
return null;
|
|
3447
|
+
}
|
|
3448
|
+
const byKey = groupByKey(skills, (s3) => s3.product ?? MISC);
|
|
3449
|
+
return normalizeAndGate(byKey, skills.length);
|
|
3450
|
+
}
|
|
3451
|
+
function partitionByNamePrefix(skills) {
|
|
3452
|
+
const keyOf = strippedKeyResolver(skills, (name) => dropTrailingLanguage(name));
|
|
3453
|
+
return normalizeAndGate(groupByKey(skills, keyOf), skills.length);
|
|
3454
|
+
}
|
|
3455
|
+
function partitionByDirectory(skills) {
|
|
3456
|
+
const keyOf = strippedKeyResolver(skills, (name) => name.split("-")[0] ?? name);
|
|
3457
|
+
return normalizeAndGate(groupByKey(skills, keyOf), skills.length);
|
|
3458
|
+
}
|
|
3459
|
+
var strippedCache = new WeakMap;
|
|
3460
|
+
function strippedKeyResolver(skills, keyFromStripped) {
|
|
3461
|
+
let stripped = strippedCache.get(skills);
|
|
3462
|
+
if (stripped === undefined) {
|
|
3463
|
+
stripped = stripCommonPrefix(skills.map((s3) => s3.dir));
|
|
3464
|
+
strippedCache.set(skills, stripped);
|
|
3465
|
+
}
|
|
3466
|
+
const keyByDir = new Map;
|
|
3467
|
+
skills.forEach((s3, i3) => {
|
|
3468
|
+
const name = stripped[i3] ?? s3.dir;
|
|
3469
|
+
const key = keyFromStripped(name);
|
|
3470
|
+
keyByDir.set(s3.dir, key === "" ? name : key);
|
|
3471
|
+
});
|
|
3472
|
+
return (s3) => keyByDir.get(s3.dir) ?? s3.dir;
|
|
3473
|
+
}
|
|
3474
|
+
function dropTrailingLanguage(name) {
|
|
3475
|
+
const parts = name.split("-");
|
|
3476
|
+
if (parts.length > 1 && LANGUAGES.has(parts[parts.length - 1] ?? "")) {
|
|
3477
|
+
parts.pop();
|
|
3478
|
+
}
|
|
3479
|
+
return parts.join("-");
|
|
3480
|
+
}
|
|
3481
|
+
function groupByKey(skills, keyOf) {
|
|
3482
|
+
const m2 = new Map;
|
|
3483
|
+
for (const s3 of skills) {
|
|
3484
|
+
const k3 = keyOf(s3);
|
|
3485
|
+
const list = m2.get(k3) ?? [];
|
|
3486
|
+
list.push(s3);
|
|
3487
|
+
m2.set(k3, list);
|
|
3488
|
+
}
|
|
3489
|
+
return m2;
|
|
3490
|
+
}
|
|
3491
|
+
function passesGateBounds(groups, total) {
|
|
3492
|
+
return groups.length >= MIN_K && groups.length <= MAX_K && !groups.some((g) => g.skills.length > MAX_FRACTION * total);
|
|
3493
|
+
}
|
|
3494
|
+
function normalizeAndGate(rawByKey, total) {
|
|
3495
|
+
let groups = [...rawByKey].map(([key, skills]) => ({ key, skills }));
|
|
3496
|
+
if (groups.length === 0) {
|
|
3497
|
+
return null;
|
|
3498
|
+
}
|
|
3499
|
+
groups = collapseToFit(groups, MAX_K);
|
|
3500
|
+
groups = coalesceTiny(groups, MIN_GROUP_SIZE);
|
|
3501
|
+
if (!passesGateBounds(groups, total)) {
|
|
3502
|
+
return null;
|
|
3503
|
+
}
|
|
3504
|
+
return finalizeGroups(groups.map((g) => ({ slug: slugify(g.key), skills: g.skills })));
|
|
3505
|
+
}
|
|
3506
|
+
function collapseToFit(groups, maxK) {
|
|
3507
|
+
if (groups.length <= maxK) {
|
|
3508
|
+
return groups;
|
|
3509
|
+
}
|
|
3510
|
+
const collapsed = collapseByLeadingSegment(groups);
|
|
3511
|
+
if (collapsed.length <= maxK) {
|
|
3512
|
+
return collapsed;
|
|
3513
|
+
}
|
|
3514
|
+
return mergeSmallestIntoMisc(collapsed, maxK);
|
|
3515
|
+
}
|
|
3516
|
+
function collapseByLeadingSegment(groups) {
|
|
3517
|
+
const m2 = new Map;
|
|
3518
|
+
for (const g of groups) {
|
|
3519
|
+
const lead = g.key.split("-")[0] ?? g.key;
|
|
3520
|
+
const list = m2.get(lead) ?? [];
|
|
3521
|
+
list.push(...g.skills);
|
|
3522
|
+
m2.set(lead, list);
|
|
3523
|
+
}
|
|
3524
|
+
return [...m2].map(([key, skills]) => ({ key, skills }));
|
|
3525
|
+
}
|
|
3526
|
+
function appendToMisc(keep, extra) {
|
|
3527
|
+
const existingMisc = keep.find((g) => g.key === MISC);
|
|
3528
|
+
if (existingMisc !== undefined) {
|
|
3529
|
+
existingMisc.skills.push(...extra);
|
|
3530
|
+
return [...keep];
|
|
3531
|
+
}
|
|
3532
|
+
return [...keep, { key: MISC, skills: extra }];
|
|
3533
|
+
}
|
|
3534
|
+
function mergeSmallestIntoMisc(groups, maxK) {
|
|
3535
|
+
const sorted = [...groups].sort((a3, b6) => a3.skills.length - b6.skills.length || byCodeUnit(a3.key, b6.key));
|
|
3536
|
+
const removeCount = groups.length - maxK + 1;
|
|
3537
|
+
const toMerge = sorted.slice(0, removeCount);
|
|
3538
|
+
const keep = sorted.slice(removeCount);
|
|
3539
|
+
return appendToMisc(keep, toMerge.flatMap((g) => g.skills));
|
|
3540
|
+
}
|
|
3541
|
+
function coalesceTiny(groups, minSize) {
|
|
3542
|
+
const tiny = groups.filter((g) => g.skills.length < minSize);
|
|
3543
|
+
if (tiny.length === 0) {
|
|
3544
|
+
return [...groups];
|
|
3545
|
+
}
|
|
3546
|
+
const big = groups.filter((g) => g.skills.length >= minSize);
|
|
3547
|
+
return appendToMisc(big, tiny.flatMap((g) => g.skills));
|
|
3548
|
+
}
|
|
3549
|
+
function finalizeGroups(groups) {
|
|
3550
|
+
const slugs = uniqueSlugs(groups.map((g) => g.slug));
|
|
3551
|
+
return groups.map((g, i3) => ({
|
|
3552
|
+
slug: slugs[i3] ?? g.slug,
|
|
3553
|
+
skills: [...g.skills].sort((a3, b6) => byCodeUnit(a3.dir, b6.dir))
|
|
3554
|
+
})).sort((a3, b6) => byCodeUnit(a3.slug, b6.slug));
|
|
3555
|
+
}
|
|
3556
|
+
function acceptRawGroups(groups, total) {
|
|
3557
|
+
if (!passesGateBounds(groups, total)) {
|
|
3558
|
+
return null;
|
|
3559
|
+
}
|
|
3560
|
+
const covered = groups.flatMap((g) => g.skills.map((s3) => s3.dir));
|
|
3561
|
+
if (new Set(covered).size !== covered.length) {
|
|
3562
|
+
return null;
|
|
3563
|
+
}
|
|
3564
|
+
if (covered.length !== total) {
|
|
3565
|
+
return null;
|
|
3566
|
+
}
|
|
3567
|
+
return finalizeGroups(groups);
|
|
3568
|
+
}
|
|
3569
|
+
function mapRawGroups(skills, raw) {
|
|
3570
|
+
const byDir = new Map(skills.map((s3) => [s3.dir, s3]));
|
|
3571
|
+
const out = [];
|
|
3572
|
+
for (const item of raw) {
|
|
3573
|
+
const gs = item.members.map((m2) => byDir.get(m2)).filter((s3) => s3 !== undefined);
|
|
3574
|
+
if (gs.length > 0) {
|
|
3575
|
+
out.push({ slug: slugify(item.slug), skills: gs });
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
return out;
|
|
3579
|
+
}
|
|
3580
|
+
function validRawItems(raw) {
|
|
3581
|
+
return raw.filter((g) => safeParse(RawGroupSchema, g).success);
|
|
3582
|
+
}
|
|
3583
|
+
function gateRaw(skills, raw) {
|
|
3584
|
+
return acceptRawGroups(mapRawGroups(skills, raw), skills.length);
|
|
3585
|
+
}
|
|
3586
|
+
function matchMarkerGroups(skills, markerGroups) {
|
|
3587
|
+
const byPath = new Map(skills.map((s3) => [normalizePath2(s3.path), s3]));
|
|
3588
|
+
const byDir = new Map(skills.map((s3) => [s3.dir, s3]));
|
|
3589
|
+
const assigned = new Set;
|
|
3590
|
+
const out = [];
|
|
3591
|
+
const unmatched = [];
|
|
3592
|
+
const duplicates = [];
|
|
3593
|
+
const fuzzyMatched = [];
|
|
3594
|
+
for (const mg of markerGroups) {
|
|
3595
|
+
const gs = [];
|
|
3596
|
+
for (const p2 of mg.skills) {
|
|
3597
|
+
const norm = normalizePath2(p2);
|
|
3598
|
+
const exact = byPath.get(norm);
|
|
3599
|
+
const s3 = exact ?? byDir.get(basename2(norm));
|
|
3600
|
+
if (s3 === undefined) {
|
|
3601
|
+
unmatched.push(p2);
|
|
3602
|
+
continue;
|
|
3603
|
+
}
|
|
3604
|
+
if (exact === undefined) {
|
|
3605
|
+
fuzzyMatched.push(p2);
|
|
3606
|
+
}
|
|
3607
|
+
if (assigned.has(s3.dir)) {
|
|
3608
|
+
duplicates.push(p2);
|
|
3609
|
+
continue;
|
|
3610
|
+
}
|
|
3611
|
+
gs.push(s3);
|
|
3612
|
+
assigned.add(s3.dir);
|
|
3613
|
+
}
|
|
3614
|
+
if (gs.length > 0) {
|
|
3615
|
+
out.push({ slug: slugify(mg.slug), skills: gs });
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
return { groups: out, unmatched, duplicates, fuzzyMatched, leftover: skills.filter((s3) => !assigned.has(s3.dir)) };
|
|
3619
|
+
}
|
|
3620
|
+
function preview(items) {
|
|
3621
|
+
return items.slice(0, 3).join(", ") + (items.length > 3 ? ", …" : "");
|
|
3622
|
+
}
|
|
3623
|
+
function normalizePath2(p2) {
|
|
3624
|
+
return p2.endsWith("/") ? p2 : `${p2}/`;
|
|
3625
|
+
}
|
|
3626
|
+
var deterministicFns = {
|
|
3627
|
+
metadata: partitionByMetadata,
|
|
3628
|
+
directory: partitionByDirectory,
|
|
3629
|
+
"name-prefix": partitionByNamePrefix
|
|
3630
|
+
};
|
|
3631
|
+
function deterministicCascade(skills) {
|
|
3632
|
+
for (const name of ["metadata", "directory", "name-prefix"]) {
|
|
3633
|
+
const groups = deterministicFns[name](skills);
|
|
3634
|
+
if (groups !== null) {
|
|
3635
|
+
return { strategy: name, groups };
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
return null;
|
|
3639
|
+
}
|
|
3640
|
+
async function partitionSkills(skills, options = {}) {
|
|
3641
|
+
const strategy = options.strategy ?? DEFAULT_STRATEGY;
|
|
3642
|
+
const warnings = [];
|
|
3643
|
+
let llmFailure;
|
|
3644
|
+
const deterministic = (d2) => d2 === null ? { groups: null, provenance: { kind: "none", ...llmFailure !== undefined ? { llmFailure } : {} }, warnings } : {
|
|
3645
|
+
groups: d2.groups,
|
|
3646
|
+
provenance: { kind: "deterministic", strategy: d2.strategy, ...llmFailure !== undefined ? { llmFailure } : {} },
|
|
3647
|
+
warnings
|
|
3648
|
+
};
|
|
3649
|
+
if (options.markerGroups !== undefined && options.markerGroups.length > 0) {
|
|
3650
|
+
const { groups: matched, unmatched, duplicates, fuzzyMatched, leftover } = matchMarkerGroups(skills, options.markerGroups);
|
|
3651
|
+
if (matched.length === 0) {
|
|
3652
|
+
warnings.push(`no path in .ccpluginizer.json "groups" matched a skill directory (expected paths like "./<skill-dir>/"); ignoring the frozen split — re-run scan --write-marker to refresh it.`);
|
|
3653
|
+
if (options.markerMandatory === true) {
|
|
3654
|
+
return { groups: null, provenance: { kind: "none" }, warnings };
|
|
3655
|
+
}
|
|
3656
|
+
} else {
|
|
3657
|
+
if (unmatched.length > 0) {
|
|
3658
|
+
warnings.push(`${String(unmatched.length)} path(s) in .ccpluginizer.json "groups" match no skill directory (${preview(unmatched)}); re-run scan --write-marker to refresh the frozen split.`);
|
|
3659
|
+
}
|
|
3660
|
+
if (fuzzyMatched.length > 0) {
|
|
3661
|
+
warnings.push(`${String(fuzzyMatched.length)} path(s) in .ccpluginizer.json "groups" matched a skill by directory name only (${preview(fuzzyMatched)}); update the marker paths or re-run scan --write-marker.`);
|
|
3662
|
+
}
|
|
3663
|
+
if (duplicates.length > 0) {
|
|
3664
|
+
warnings.push(`${String(duplicates.length)} path(s) in .ccpluginizer.json "groups" appear in more than one group; the first occurrence wins (${preview(duplicates)}) — fix the marker or re-run scan --write-marker.`);
|
|
3665
|
+
}
|
|
3666
|
+
let groups2 = matched;
|
|
3667
|
+
if (leftover.length > 0) {
|
|
3668
|
+
warnings.push(`${String(leftover.length)} skill(s) not listed in .ccpluginizer.json "groups" were placed in a "${MISC}" slice; re-run scan --write-marker to refresh the frozen split.`);
|
|
3669
|
+
groups2 = [...matched, { slug: MISC, skills: leftover }];
|
|
3670
|
+
}
|
|
3671
|
+
return { groups: finalizeGroups(groups2), provenance: { kind: "marker" }, warnings };
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
const runLlm = async () => {
|
|
3675
|
+
if (options.group === undefined) {
|
|
3676
|
+
llmFailure = { step: "no-backend" };
|
|
3677
|
+
return null;
|
|
3678
|
+
}
|
|
3679
|
+
let run;
|
|
3680
|
+
try {
|
|
3681
|
+
run = await options.group(skills);
|
|
3682
|
+
} catch {
|
|
3683
|
+
llmFailure = { step: "errored" };
|
|
3684
|
+
return null;
|
|
3685
|
+
}
|
|
3686
|
+
if (run === null) {
|
|
3687
|
+
llmFailure = { step: "no-backend" };
|
|
3688
|
+
return null;
|
|
3689
|
+
}
|
|
3690
|
+
const raw = validRawItems(run.groups);
|
|
3691
|
+
if (raw.length === 0) {
|
|
3692
|
+
llmFailure = { step: "no-output", backend: run.kind };
|
|
3693
|
+
return null;
|
|
3694
|
+
}
|
|
3695
|
+
const gated = gateRaw(skills, raw);
|
|
3696
|
+
if (gated === null) {
|
|
3697
|
+
llmFailure = { step: "gate-rejected", backend: run.kind };
|
|
3698
|
+
return null;
|
|
3699
|
+
}
|
|
3700
|
+
run.commit?.();
|
|
3701
|
+
return { backend: run.kind, groups: gated };
|
|
3702
|
+
};
|
|
3703
|
+
if (strategy === "auto") {
|
|
3704
|
+
return deterministic(deterministicCascade(skills));
|
|
3705
|
+
}
|
|
3706
|
+
if (strategy === "llm") {
|
|
3707
|
+
const won = await runLlm();
|
|
3708
|
+
if (won !== null) {
|
|
3709
|
+
return { groups: won.groups, provenance: { kind: "llm", backend: won.backend }, warnings };
|
|
3710
|
+
}
|
|
3711
|
+
return deterministic(deterministicCascade(skills));
|
|
3712
|
+
}
|
|
3713
|
+
if (strategy === "auto-llm") {
|
|
3714
|
+
const d2 = deterministicCascade(skills);
|
|
3715
|
+
if (d2 !== null) {
|
|
3716
|
+
return deterministic(d2);
|
|
3717
|
+
}
|
|
3718
|
+
const won = await runLlm();
|
|
3719
|
+
if (won !== null) {
|
|
3720
|
+
return { groups: won.groups, provenance: { kind: "llm", backend: won.backend }, warnings };
|
|
3721
|
+
}
|
|
3722
|
+
return deterministic(null);
|
|
3723
|
+
}
|
|
3724
|
+
const groups = deterministicFns[strategy](skills);
|
|
3725
|
+
return deterministic(groups === null ? null : { strategy, groups });
|
|
3726
|
+
}
|
|
3727
|
+
|
|
2011
3728
|
// src/detector/synthesize.ts
|
|
2012
|
-
|
|
3729
|
+
var DEFAULT_MIN_SKILLS_TO_SPLIT = 25;
|
|
3730
|
+
var SKIPPED = { kind: "skipped" };
|
|
3731
|
+
async function synthesizeEntries(input) {
|
|
3732
|
+
const wantSplit = input.split ?? true;
|
|
3733
|
+
const marker = input.existingMarker !== undefined ? input.existingMarker : detectMarkerFile(input.repoRoot);
|
|
3734
|
+
const caches = input.caches ?? makeScanCaches();
|
|
3735
|
+
const skippedPaths = caches.skippedPaths ?? [];
|
|
3736
|
+
const skipWarnings = () => skippedPaths.length > 0 ? [
|
|
3737
|
+
`${String(skippedPaths.length)} unreadable path(s) were skipped during detection (permission denied), e.g. "${skippedPaths[0] ?? ""}" — the emitted entries may be incomplete.`
|
|
3738
|
+
] : [];
|
|
3739
|
+
const attempt = wantSplit && (marker === null || !markerSuppressesSplit(marker)) ? await attemptSplit(input, marker, caches, skipWarnings) : null;
|
|
3740
|
+
if (attempt !== null && attempt.fulfilled !== null) {
|
|
3741
|
+
return attempt.fulfilled;
|
|
3742
|
+
}
|
|
2013
3743
|
checkMarketplaceGuard(input.repoRoot);
|
|
2014
|
-
const
|
|
3744
|
+
const entries = [synthesizeEntryWithMarker(input, marker, caches)];
|
|
3745
|
+
return {
|
|
3746
|
+
entries,
|
|
3747
|
+
split: null,
|
|
3748
|
+
provenance: attempt?.provenance ?? SKIPPED,
|
|
3749
|
+
marker: null,
|
|
3750
|
+
existingMarker: marker,
|
|
3751
|
+
warnings: [...attempt?.warnings ?? [], ...skipWarnings()],
|
|
3752
|
+
caches
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
async function attemptSplit(input, marker, caches, skipWarnings) {
|
|
3756
|
+
const minSkills = input.minSkillsToSplit ?? DEFAULT_MIN_SKILLS_TO_SPLIT;
|
|
3757
|
+
const resolver = createLayoutResolver(input.repoRoot, caches);
|
|
3758
|
+
const none = (provenance2, warnings) => ({
|
|
3759
|
+
fulfilled: null,
|
|
3760
|
+
provenance: provenance2,
|
|
3761
|
+
warnings: [...resolver.warnings, ...warnings]
|
|
3762
|
+
});
|
|
3763
|
+
const frozen = marker !== null;
|
|
3764
|
+
if (resolver.skillsContainer === null) {
|
|
3765
|
+
return none(SKIPPED, frozen ? [".ccpluginizer.json freezes a split, but no skills container was found; ignoring the frozen groups and emitting a single entry."] : []);
|
|
3766
|
+
}
|
|
3767
|
+
const skills = enumerateSkills(resolver.skillsContainer.absDir, resolver.list, resolver.readFrontmatter);
|
|
3768
|
+
if (!frozen && skills.length < minSkills) {
|
|
3769
|
+
return none(SKIPPED, []);
|
|
3770
|
+
}
|
|
3771
|
+
const unparsed = countSkillMdDirs(resolver.skillsContainer.absDir, resolver.list) - skills.length;
|
|
3772
|
+
const unparsedWarnings = unparsed > 0 ? [
|
|
3773
|
+
`${String(unparsed)} skill ${unparsed === 1 ? "directory" : "directories"} inside "${resolver.skillsContainer.relPath}" ${unparsed === 1 ? "has" : "have"} missing or invalid SKILL.md frontmatter and ${unparsed === 1 ? "is" : "are"} not covered by any emitted slice; fix the frontmatter so they are detected.`
|
|
3774
|
+
] : [];
|
|
3775
|
+
const recuration = isAlreadyMarketplace(input.repoRoot);
|
|
3776
|
+
const recurationWarnings = recuration ? [
|
|
3777
|
+
"this repo already publishes a marketplace (.claude-plugin/marketplace.json); the split re-curates it deterministically (the LLM step is never consulted here). Install via `/plugin marketplace add` instead if you just want the existing catalog."
|
|
3778
|
+
] : [];
|
|
3779
|
+
const markerMandatory = frozen && skills.length < minSkills;
|
|
3780
|
+
const { groups, provenance, warnings: partitionWarnings } = await partitionSkills(skills, {
|
|
3781
|
+
strategy: input.strategy ?? DEFAULT_STRATEGY,
|
|
3782
|
+
...frozen ? { markerGroups: marker.groups ?? [] } : {},
|
|
3783
|
+
...input.group !== undefined && !recuration ? { group: input.group } : {},
|
|
3784
|
+
...markerMandatory ? { markerMandatory: true } : {}
|
|
3785
|
+
});
|
|
3786
|
+
if (groups === null) {
|
|
3787
|
+
return none(markerMandatory ? SKIPPED : provenance, [...partitionWarnings, ...unparsedWarnings]);
|
|
3788
|
+
}
|
|
3789
|
+
const layout = resolver.full();
|
|
3790
|
+
const markerDrives = provenance.kind === "marker";
|
|
3791
|
+
const componentCurationWarnings = markerDrives && marker !== null && hasComponentCuration(marker) ? [
|
|
3792
|
+
"the component fields in .ccpluginizer.json (skills/agents/mcpServers/...) apply only to the single-entry path; the split derives its core from the repo layout."
|
|
3793
|
+
] : [];
|
|
3794
|
+
const { entries, marker: markerDraft, agentsDropped, coreEmitted } = buildSplitEntries(groups, layout, resolver.skillsContainer, input.sourceRepo, {
|
|
3795
|
+
umbrella: (input.umbrella ?? false) || markerDrives && (marker?.umbrella ?? false),
|
|
3796
|
+
emitCore: markerDrives ? marker?.core ?? true : true,
|
|
3797
|
+
...markerDrives && marker?.name !== undefined ? { markerName: marker.name } : {}
|
|
3798
|
+
});
|
|
3799
|
+
return {
|
|
3800
|
+
fulfilled: {
|
|
3801
|
+
entries,
|
|
3802
|
+
split: { groupCount: groups.length, coreEmitted, umbrellaEmitted: markerDraft.umbrella },
|
|
3803
|
+
provenance,
|
|
3804
|
+
marker: markerDraft,
|
|
3805
|
+
existingMarker: marker,
|
|
3806
|
+
warnings: [
|
|
3807
|
+
...resolver.warnings,
|
|
3808
|
+
...partitionWarnings,
|
|
3809
|
+
...unparsedWarnings,
|
|
3810
|
+
...componentCurationWarnings,
|
|
3811
|
+
...recurationWarnings,
|
|
3812
|
+
...collectSplitWarnings(layout, input.sourceRepo, coreEmitted, markerDraft.umbrella, agentsDropped),
|
|
3813
|
+
...skipWarnings()
|
|
3814
|
+
],
|
|
3815
|
+
caches
|
|
3816
|
+
}
|
|
3817
|
+
};
|
|
3818
|
+
}
|
|
3819
|
+
function collectSplitWarnings(layout, sourceRepo, coreEmitted, umbrellaEmitted, agentsDropped) {
|
|
3820
|
+
const warnings = [];
|
|
3821
|
+
if (layout.mcp !== null) {
|
|
3822
|
+
if (coreEmitted && layout.mcp.serverType === "repo-local") {
|
|
3823
|
+
warnings.push(`The MCP server in "${layout.mcp.relPath}" references repo-local files; inlined into core it may not resolve at install time. Review its command/args paths, or keep the MCP via --umbrella.`);
|
|
3824
|
+
} else if (!coreEmitted && !umbrellaEmitted) {
|
|
3825
|
+
warnings.push(`The MCP server in "${layout.mcp.relPath}" is not carried by any emitted entry (no core entry was emitted and no umbrella was requested); it will be dropped. Use --umbrella, or a marker with "core": true, to retain it.`);
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
const umbrellaRootForAgents = layout.pluginRoot?.relPath ?? ".";
|
|
3829
|
+
const umbrellaCarriesAgents = umbrellaEmitted && layout.agentsContainer !== null && (umbrellaRootForAgents === "." || layout.agentsContainer.relPath === umbrellaRootForAgents || layout.agentsContainer.relPath.startsWith(`${umbrellaRootForAgents}/`));
|
|
3830
|
+
if (layout.agentsContainer !== null && !umbrellaCarriesAgents) {
|
|
3831
|
+
if (agentsDropped) {
|
|
3832
|
+
warnings.push(`The agents in "${layout.agentsContainer.relPath}" are not carried by the core entry (no safe core root exists — rooting it would auto-load a skills/ tree always-on); they will be dropped. Use --umbrella to retain them.`);
|
|
3833
|
+
} else if (!coreEmitted) {
|
|
3834
|
+
warnings.push(`The agents in "${layout.agentsContainer.relPath}" are not carried by any emitted entry (no core entry was emitted and no umbrella was requested); they will be dropped. Use --umbrella, or a marker with "core": true, to retain them.`);
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
const umbrellaRoot = layout.pluginRoot?.relPath ?? ".";
|
|
3838
|
+
const underUmbrella = (rel) => umbrellaRoot === "." || rel === umbrellaRoot || rel.startsWith(`${umbrellaRoot}/`);
|
|
3839
|
+
const carriable = layout.artifacts.filter((a3) => underUmbrella(a3.relPath));
|
|
3840
|
+
const outside = layout.artifacts.filter((a3) => !underUmbrella(a3.relPath));
|
|
3841
|
+
if (!umbrellaEmitted && carriable.length > 0) {
|
|
3842
|
+
warnings.push(`Split entries do not carry these non-skill artifacts: ${carriable.map((a3) => a3.kind).join(", ")}. Use --umbrella to retain them, or --no-split for a single entry.`);
|
|
3843
|
+
}
|
|
3844
|
+
if (outside.length > 0) {
|
|
3845
|
+
warnings.push(`These non-skill artifacts sit outside the plugin root, so no emitted entry (umbrella included) carries them: ${outside.map((a3) => a3.kind).join(", ")}. Use --no-split for a single entry.`);
|
|
3846
|
+
}
|
|
3847
|
+
if (layout.skillDirsOutsideContainer > 0 && layout.skillsContainer !== null) {
|
|
3848
|
+
const n2 = layout.skillDirsOutsideContainer;
|
|
3849
|
+
warnings.push(`${String(n2)} skill ${n2 === 1 ? "directory" : "directories"} outside "${layout.skillsContainer.relPath}" ${n2 === 1 ? "is" : "are"} not covered by the split; use --no-split for a single entry that detects the whole repo.`);
|
|
3850
|
+
}
|
|
3851
|
+
if (sourceRepo.startsWith("local/")) {
|
|
3852
|
+
warnings.push(`Source is a local path; emitted git-subdir URLs are placeholders ("https://github.com/${sourceRepo}.git"). Set the real repository before publishing.`);
|
|
3853
|
+
}
|
|
3854
|
+
return warnings;
|
|
3855
|
+
}
|
|
3856
|
+
function synthesizeEntryWithMarker(input, marker, caches) {
|
|
2015
3857
|
if (marker !== null) {
|
|
3858
|
+
if (isFreezeOnlyMarker(marker)) {
|
|
3859
|
+
return {
|
|
3860
|
+
...synthesizeEntryFromDetection(input, caches),
|
|
3861
|
+
name: marker.name,
|
|
3862
|
+
...markerIdentity(marker)
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
2016
3865
|
return buildEntryFromMarker(marker, input.sourceRepo, input.repoRoot);
|
|
2017
3866
|
}
|
|
2018
|
-
|
|
3867
|
+
return synthesizeEntryFromDetection(input, caches);
|
|
3868
|
+
}
|
|
3869
|
+
function markerIdentity(marker) {
|
|
3870
|
+
return {
|
|
3871
|
+
...marker.description !== undefined ? { description: marker.description } : {},
|
|
3872
|
+
...marker.license !== undefined ? { license: marker.license } : {},
|
|
3873
|
+
...marker.homepage !== undefined ? { homepage: marker.homepage } : {},
|
|
3874
|
+
...marker.repository !== undefined ? { repository: marker.repository } : {}
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
function synthesizeEntryFromDetection(input, caches) {
|
|
3878
|
+
const conventionFindings = detectConventions(input.repoRoot, caches);
|
|
2019
3879
|
const manifestResult = detectNonStandardManifest(input.repoRoot);
|
|
2020
|
-
const sniffFindings = detectContentSniff(input.repoRoot);
|
|
3880
|
+
const sniffFindings = detectContentSniff(input.repoRoot, caches);
|
|
2021
3881
|
const findings = [...conventionFindings];
|
|
2022
3882
|
const manifestKindsWithFindings = new Set;
|
|
2023
3883
|
if (manifestResult !== null) {
|
|
2024
3884
|
addManifestFindings(findings, manifestResult.manifest, manifestKindsWithFindings);
|
|
2025
3885
|
}
|
|
2026
|
-
let mergedFindings = findings.filter((
|
|
2027
|
-
if (
|
|
3886
|
+
let mergedFindings = findings.filter((f2) => {
|
|
3887
|
+
if (f2.source === "convention" && manifestKindsWithFindings.has(f2.kind)) {
|
|
2028
3888
|
return false;
|
|
2029
3889
|
}
|
|
2030
3890
|
return true;
|
|
@@ -2086,10 +3946,7 @@ function buildEntryFromMarker(marker, sourceRepo, repoRoot) {
|
|
|
2086
3946
|
name: marker.name,
|
|
2087
3947
|
source: makeGithubSource(sourceRepo),
|
|
2088
3948
|
strict: false,
|
|
2089
|
-
...marker
|
|
2090
|
-
...marker.license !== undefined ? { license: marker.license } : {},
|
|
2091
|
-
...marker.homepage !== undefined ? { homepage: marker.homepage } : {},
|
|
2092
|
-
...marker.repository !== undefined ? { repository: marker.repository } : {},
|
|
3949
|
+
...markerIdentity(marker),
|
|
2093
3950
|
...skills !== undefined ? { skills } : {},
|
|
2094
3951
|
...agents !== undefined ? { agents } : {},
|
|
2095
3952
|
...commands !== undefined ? { commands } : {},
|
|
@@ -2104,7 +3961,7 @@ function buildEntryFromFindings(findings, repoRoot, sourceRepo) {
|
|
|
2104
3961
|
const byKind = groupByKind(findings);
|
|
2105
3962
|
const componentEntries = {};
|
|
2106
3963
|
for (const [kind, kindFindings] of byKind) {
|
|
2107
|
-
const allPaths = kindFindings.flatMap((
|
|
3964
|
+
const allPaths = kindFindings.flatMap((f2) => f2.paths);
|
|
2108
3965
|
const { kept } = normalizePathsAgainstRepo(repoRoot, allPaths);
|
|
2109
3966
|
if (kept.length > 0) {
|
|
2110
3967
|
componentEntries[kind] = kept;
|
|
@@ -2118,8 +3975,8 @@ function buildEntryFromFindings(findings, repoRoot, sourceRepo) {
|
|
|
2118
3975
|
};
|
|
2119
3976
|
}
|
|
2120
3977
|
function hasKind(findings, kind) {
|
|
2121
|
-
for (const
|
|
2122
|
-
if (
|
|
3978
|
+
for (const f2 of findings) {
|
|
3979
|
+
if (f2.kind === kind) {
|
|
2123
3980
|
return true;
|
|
2124
3981
|
}
|
|
2125
3982
|
}
|
|
@@ -2127,95 +3984,828 @@ function hasKind(findings, kind) {
|
|
|
2127
3984
|
}
|
|
2128
3985
|
function groupByKind(findings) {
|
|
2129
3986
|
const map = new Map;
|
|
2130
|
-
for (const
|
|
2131
|
-
const list = map.get(
|
|
2132
|
-
list.push(
|
|
2133
|
-
map.set(
|
|
3987
|
+
for (const f2 of findings) {
|
|
3988
|
+
const list = map.get(f2.kind) ?? [];
|
|
3989
|
+
list.push(f2);
|
|
3990
|
+
map.set(f2.kind, list);
|
|
2134
3991
|
}
|
|
2135
3992
|
return map;
|
|
2136
3993
|
}
|
|
3994
|
+
function sourceRepoUrl(repo) {
|
|
3995
|
+
return `https://github.com/${repo}.git`;
|
|
3996
|
+
}
|
|
2137
3997
|
function makeGithubSource(repo) {
|
|
2138
|
-
return { source: "url", url:
|
|
3998
|
+
return { source: "url", url: sourceRepoUrl(repo) };
|
|
3999
|
+
}
|
|
4000
|
+
function makeGitSubdirSource(repo, path) {
|
|
4001
|
+
return { source: "git-subdir", url: sourceRepoUrl(repo), path };
|
|
2139
4002
|
}
|
|
2140
4003
|
function defaultEntryName(sourceRepo) {
|
|
2141
|
-
return sourceRepo
|
|
4004
|
+
return slugify(sourceRepo);
|
|
4005
|
+
}
|
|
4006
|
+
function serializeMarkerDraft(draft, existing) {
|
|
4007
|
+
const draftOwned = new Set(Object.keys(draft));
|
|
4008
|
+
const preserved = {};
|
|
4009
|
+
if (existing !== null) {
|
|
4010
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
4011
|
+
if (!draftOwned.has(key) && value !== undefined) {
|
|
4012
|
+
preserved[key] = value;
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
const merged = {
|
|
4017
|
+
...draft,
|
|
4018
|
+
...draft.umbrella ? {} : { umbrella: undefined },
|
|
4019
|
+
...preserved
|
|
4020
|
+
};
|
|
4021
|
+
parse(MarkerFileSchema, JSON.parse(JSON.stringify(merged)));
|
|
4022
|
+
return merged;
|
|
4023
|
+
}
|
|
4024
|
+
function buildSplitEntries(groups, layout, skillsContainer, sourceRepo, options) {
|
|
4025
|
+
const base = slugify(options.markerName ?? defaultEntryName(sourceRepo));
|
|
4026
|
+
const entries = [];
|
|
4027
|
+
if (options.umbrella) {
|
|
4028
|
+
entries.push(buildUmbrellaEntry(base, groups, layout, skillsContainer, sourceRepo));
|
|
4029
|
+
}
|
|
4030
|
+
const core = options.emitCore ? buildCoreEntry(`${base}-core`, layout, skillsContainer, sourceRepo) : { entry: null, agentsDropped: false };
|
|
4031
|
+
if (core.entry !== null) {
|
|
4032
|
+
entries.push(core.entry);
|
|
4033
|
+
}
|
|
4034
|
+
const reservedSlugs = core.entry !== null ? ["core"] : [];
|
|
4035
|
+
const slugs = uniqueSlugs([...reservedSlugs, ...groups.map((g) => g.slug)]).slice(reservedSlugs.length);
|
|
4036
|
+
const markerGroups = [];
|
|
4037
|
+
groups.forEach((group, i3) => {
|
|
4038
|
+
const slug = slugs[i3] ?? group.slug;
|
|
4039
|
+
const skills = group.skills.map((s3) => s3.path);
|
|
4040
|
+
markerGroups.push({ slug, skills });
|
|
4041
|
+
entries.push({
|
|
4042
|
+
name: `${base}-${slug}`,
|
|
4043
|
+
source: makeGitSubdirSource(sourceRepo, skillsContainer.relPath),
|
|
4044
|
+
strict: false,
|
|
4045
|
+
skills,
|
|
4046
|
+
...core.entry !== null ? { dependencies: [core.entry.name] } : {}
|
|
4047
|
+
});
|
|
4048
|
+
});
|
|
4049
|
+
return {
|
|
4050
|
+
entries,
|
|
4051
|
+
marker: { name: base, core: options.emitCore, umbrella: options.umbrella, groups: markerGroups },
|
|
4052
|
+
agentsDropped: core.agentsDropped,
|
|
4053
|
+
coreEmitted: core.entry !== null
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
4056
|
+
function buildUmbrellaEntry(base, groups, layout, skillsContainer, sourceRepo) {
|
|
4057
|
+
const root = layout.pluginRoot?.relPath;
|
|
4058
|
+
const rootContainsSkills = root !== undefined && (root === "." || skillsContainer.relPath === root || skillsContainer.relPath.startsWith(`${root}/`));
|
|
4059
|
+
if (root !== undefined && rootContainsSkills) {
|
|
4060
|
+
return {
|
|
4061
|
+
name: base,
|
|
4062
|
+
source: makeGitSubdirSource(sourceRepo, root),
|
|
4063
|
+
strict: true
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
4066
|
+
const artifact = (kind) => layout.artifacts.find((a3) => a3.kind === kind)?.relPath;
|
|
4067
|
+
const skills = groups.flatMap((g) => g.skills).map((s3) => repoRelPath(skillsContainer.relPath, s3.path)).sort(byCodeUnit);
|
|
4068
|
+
const agents = layout.agentsContainer;
|
|
4069
|
+
const hooks = artifact("hooks");
|
|
4070
|
+
const commands = artifact("commands");
|
|
4071
|
+
const outputStyles = artifact("output-styles");
|
|
4072
|
+
const themes = artifact("themes");
|
|
4073
|
+
const monitors = artifact("monitors");
|
|
4074
|
+
return {
|
|
4075
|
+
name: base,
|
|
4076
|
+
source: makeGitSubdirSource(sourceRepo, "."),
|
|
4077
|
+
strict: false,
|
|
4078
|
+
...skills.length > 0 ? { skills } : {},
|
|
4079
|
+
...agents !== null ? { agents: agents.files.map((f2) => repoRelPath(agents.relPath, f2)) } : {},
|
|
4080
|
+
...layout.mcp !== null ? { mcpServers: layout.mcp.servers } : {},
|
|
4081
|
+
...hooks !== undefined ? { hooks: `./${hooks}` } : {},
|
|
4082
|
+
...commands !== undefined ? { commands: [`./${commands}/`] } : {},
|
|
4083
|
+
...outputStyles !== undefined ? { outputStyles: [`./${outputStyles}/`] } : {},
|
|
4084
|
+
...themes !== undefined ? { themes: [`./${themes}/`] } : {},
|
|
4085
|
+
...monitors !== undefined ? { monitors: `./${monitors}` } : {}
|
|
4086
|
+
};
|
|
4087
|
+
}
|
|
4088
|
+
function repoRelPath(containerRel, p2) {
|
|
4089
|
+
const inner = p2.startsWith("./") ? p2.slice(2) : p2;
|
|
4090
|
+
return containerRel === "." ? `./${inner}` : `./${containerRel}/${inner}`;
|
|
4091
|
+
}
|
|
4092
|
+
function buildCoreEntry(name, layout, skillsContainer, sourceRepo) {
|
|
4093
|
+
const agents = layout.agentsContainer;
|
|
4094
|
+
const mcp = layout.mcp;
|
|
4095
|
+
if (agents === null && mcp === null) {
|
|
4096
|
+
return { entry: null, agentsDropped: false };
|
|
4097
|
+
}
|
|
4098
|
+
const carriesAgents = agents !== null && !agents.hasSkillsChild;
|
|
4099
|
+
const rootPath = carriesAgents ? agents.relPath : skillsContainer.hasSkillsChild ? null : skillsContainer.relPath;
|
|
4100
|
+
if (rootPath === null || !carriesAgents && mcp === null) {
|
|
4101
|
+
return { entry: null, agentsDropped: agents !== null };
|
|
4102
|
+
}
|
|
4103
|
+
return {
|
|
4104
|
+
entry: {
|
|
4105
|
+
name,
|
|
4106
|
+
source: makeGitSubdirSource(sourceRepo, rootPath),
|
|
4107
|
+
strict: false,
|
|
4108
|
+
...carriesAgents ? { agents: agents.files.map((f2) => `./${f2}`) } : {},
|
|
4109
|
+
...mcp !== null ? { mcpServers: mcp.servers } : {}
|
|
4110
|
+
},
|
|
4111
|
+
agentsDropped: agents !== null && !carriesAgents
|
|
4112
|
+
};
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
// src/detector/validateEntries.ts
|
|
4116
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
|
|
4117
|
+
import { basename as basename3, join as join10 } from "node:path";
|
|
4118
|
+
function validateEntries(items, sources) {
|
|
4119
|
+
const errors = [];
|
|
4120
|
+
const entries = [];
|
|
4121
|
+
const nameSource = new Map;
|
|
4122
|
+
const label = (i3) => sources?.[i3] ?? `entry[${String(i3)}]`;
|
|
4123
|
+
items.forEach((item, i3) => {
|
|
4124
|
+
const result = safeParse(MarketplaceEntrySchema, item);
|
|
4125
|
+
if (!result.success) {
|
|
4126
|
+
const detail = result.issues.map((issue) => {
|
|
4127
|
+
const path = getDotPath(issue);
|
|
4128
|
+
return path === null ? issue.message : `${path}: ${issue.message}`;
|
|
4129
|
+
}).join("; ");
|
|
4130
|
+
errors.push(`${label(i3)}: ${detail}`);
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
const name = result.output.name;
|
|
4134
|
+
const firstSource = nameSource.get(name);
|
|
4135
|
+
if (firstSource !== undefined) {
|
|
4136
|
+
errors.push(`duplicate entry name: ${name} (${label(i3)} also declared in ${firstSource})`);
|
|
4137
|
+
return;
|
|
4138
|
+
}
|
|
4139
|
+
nameSource.set(name, label(i3));
|
|
4140
|
+
entries.push(result.output);
|
|
4141
|
+
});
|
|
4142
|
+
return { ok: errors.length === 0, errors, entries };
|
|
4143
|
+
}
|
|
4144
|
+
function collectEntries(path, options = {}) {
|
|
4145
|
+
if (!existsSync5(path)) {
|
|
4146
|
+
throw new Error(`No such file or directory: ${path}`);
|
|
4147
|
+
}
|
|
4148
|
+
const items = [];
|
|
4149
|
+
const sources = [];
|
|
4150
|
+
const add = (parsed, fileLabel) => {
|
|
4151
|
+
const list = toEntryList(parsed);
|
|
4152
|
+
if (list.length === 0) {
|
|
4153
|
+
throw new Error(`No entries found in: ${fileLabel}`);
|
|
4154
|
+
}
|
|
4155
|
+
list.forEach((item, i3) => {
|
|
4156
|
+
items.push(item);
|
|
4157
|
+
sources.push(list.length > 1 ? `${fileLabel}[${String(i3)}]` : fileLabel);
|
|
4158
|
+
});
|
|
4159
|
+
};
|
|
4160
|
+
if (statSync3(path).isDirectory()) {
|
|
4161
|
+
const files = readdirSync3(path).filter((f2) => f2.endsWith(".json")).sort();
|
|
4162
|
+
if (files.length === 0 && options.allowEmptyDir !== true) {
|
|
4163
|
+
throw new Error(`No entry JSON files (*.json) found in directory: ${path}`);
|
|
4164
|
+
}
|
|
4165
|
+
for (const f2 of files) {
|
|
4166
|
+
add(readJsonFile(join10(path, f2)), f2);
|
|
4167
|
+
}
|
|
4168
|
+
} else {
|
|
4169
|
+
add(readJsonFile(path), basename3(path));
|
|
4170
|
+
}
|
|
4171
|
+
return { items, sources };
|
|
4172
|
+
}
|
|
4173
|
+
function toEntryList(parsed) {
|
|
4174
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
4175
|
+
}
|
|
4176
|
+
|
|
4177
|
+
// src/commands/llmGrouper.ts
|
|
4178
|
+
import { spawn, spawnSync as spawnSync2 } from "node:child_process";
|
|
4179
|
+
import { Buffer } from "node:buffer";
|
|
4180
|
+
import { createHash } from "node:crypto";
|
|
4181
|
+
import { mkdirSync, statSync as statSync4, writeFileSync } from "node:fs";
|
|
4182
|
+
import { homedir } from "node:os";
|
|
4183
|
+
import { join as join11 } from "node:path";
|
|
4184
|
+
var MAX_BUFFER_BYTES = 32 * 1024 * 1024;
|
|
4185
|
+
function buildClusterPrompt(skills) {
|
|
4186
|
+
const lines = skills.map((s3) => {
|
|
4187
|
+
const product = s3.product !== undefined ? ` [product=${s3.product}]` : "";
|
|
4188
|
+
const desc = s3.description.replace(/\s+/g, " ").trim().slice(0, 140);
|
|
4189
|
+
return `- ${s3.dir}${product}: ${desc}`;
|
|
4190
|
+
});
|
|
4191
|
+
return [
|
|
4192
|
+
"You are grouping Claude Code skills into a small number of coherent product domains.",
|
|
4193
|
+
"",
|
|
4194
|
+
"Rules:",
|
|
4195
|
+
`- Produce between ${String(MIN_K)} and ${String(MAX_K)} groups.`,
|
|
4196
|
+
"- Every skill must appear in exactly one group (disjoint, total cover).",
|
|
4197
|
+
`- No group may contain more than ~${String(Math.round(MAX_FRACTION * 100))}% of all skills.`,
|
|
4198
|
+
"- Group by product/domain meaning, not by programming language.",
|
|
4199
|
+
'- Each group needs a short kebab-case "slug" (e.g. "messaging", "voice").',
|
|
4200
|
+
"",
|
|
4201
|
+
"Skills:",
|
|
4202
|
+
...lines,
|
|
4203
|
+
"",
|
|
4204
|
+
'Respond with ONLY a JSON array, no prose: [{"slug":"...","members":["<skill-dir>",...]}, ...]'
|
|
4205
|
+
].join(`
|
|
4206
|
+
`);
|
|
4207
|
+
}
|
|
4208
|
+
var MAX_ARRAY_CANDIDATES = 50;
|
|
4209
|
+
function parseClusterResponse(text, validDirs) {
|
|
4210
|
+
let candidates = 0;
|
|
4211
|
+
for (let start = text.indexOf("[");start !== -1; start = text.indexOf("[", start + 1)) {
|
|
4212
|
+
if (++candidates > MAX_ARRAY_CANDIDATES) {
|
|
4213
|
+
break;
|
|
4214
|
+
}
|
|
4215
|
+
const end = balancedArrayEnd(text, start);
|
|
4216
|
+
if (end === -1) {
|
|
4217
|
+
continue;
|
|
4218
|
+
}
|
|
4219
|
+
let parsed;
|
|
4220
|
+
try {
|
|
4221
|
+
parsed = JSON.parse(text.slice(start, end + 1));
|
|
4222
|
+
} catch {
|
|
4223
|
+
continue;
|
|
4224
|
+
}
|
|
4225
|
+
if (!Array.isArray(parsed)) {
|
|
4226
|
+
continue;
|
|
4227
|
+
}
|
|
4228
|
+
const groups = [];
|
|
4229
|
+
for (const item of parsed) {
|
|
4230
|
+
const r2 = safeParse(RawGroupSchema, item);
|
|
4231
|
+
if (!r2.success) {
|
|
4232
|
+
continue;
|
|
4233
|
+
}
|
|
4234
|
+
const members = r2.output.members.filter((m2) => validDirs.has(m2));
|
|
4235
|
+
if (members.length > 0) {
|
|
4236
|
+
groups.push({ slug: r2.output.slug, members });
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
if (groups.length > 0) {
|
|
4240
|
+
return groups;
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
return null;
|
|
4244
|
+
}
|
|
4245
|
+
function balancedArrayEnd(text, start) {
|
|
4246
|
+
let depth = 0;
|
|
4247
|
+
let inString = false;
|
|
4248
|
+
let escaped = false;
|
|
4249
|
+
for (let i3 = start;i3 < text.length; i3++) {
|
|
4250
|
+
const ch = text[i3];
|
|
4251
|
+
if (inString) {
|
|
4252
|
+
if (escaped) {
|
|
4253
|
+
escaped = false;
|
|
4254
|
+
} else if (ch === "\\") {
|
|
4255
|
+
escaped = true;
|
|
4256
|
+
} else if (ch === '"') {
|
|
4257
|
+
inString = false;
|
|
4258
|
+
}
|
|
4259
|
+
continue;
|
|
4260
|
+
}
|
|
4261
|
+
if (ch === '"') {
|
|
4262
|
+
inString = true;
|
|
4263
|
+
} else if (ch === "[" || ch === "{") {
|
|
4264
|
+
depth++;
|
|
4265
|
+
} else if (ch === "]" || ch === "}") {
|
|
4266
|
+
depth--;
|
|
4267
|
+
if (depth === 0) {
|
|
4268
|
+
return ch === "]" ? i3 : -1;
|
|
4269
|
+
}
|
|
4270
|
+
if (depth < 0) {
|
|
4271
|
+
return -1;
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
return -1;
|
|
4276
|
+
}
|
|
4277
|
+
function validateRawGroups(parsed) {
|
|
4278
|
+
const r2 = safeParse(RawGroupsSchema, parsed);
|
|
4279
|
+
return r2.success ? r2.output : null;
|
|
4280
|
+
}
|
|
4281
|
+
function spawnGroupRun(command, args, options) {
|
|
4282
|
+
return new Promise((resolve2) => {
|
|
4283
|
+
const win = process.platform === "win32";
|
|
4284
|
+
let child;
|
|
4285
|
+
try {
|
|
4286
|
+
child = spawn(command, [...args], {
|
|
4287
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
4288
|
+
detached: !win,
|
|
4289
|
+
windowsVerbatimArguments: win
|
|
4290
|
+
});
|
|
4291
|
+
} catch (e3) {
|
|
4292
|
+
resolve2({ error: e3 instanceof Error ? e3 : new Error(String(e3)), signal: null, status: null, stdout: null });
|
|
4293
|
+
return;
|
|
4294
|
+
}
|
|
4295
|
+
let stdout = "";
|
|
4296
|
+
let stdoutBytes = 0;
|
|
4297
|
+
let timedOut = false;
|
|
4298
|
+
let overflowed = false;
|
|
4299
|
+
let settled = false;
|
|
4300
|
+
const timers = [];
|
|
4301
|
+
const killTree = (sig) => {
|
|
4302
|
+
try {
|
|
4303
|
+
if (!win && child.pid !== undefined) {
|
|
4304
|
+
process.kill(-child.pid, sig);
|
|
4305
|
+
} else if (win && child.pid !== undefined) {
|
|
4306
|
+
spawnSync2("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
4307
|
+
} else {
|
|
4308
|
+
child.kill(sig);
|
|
4309
|
+
}
|
|
4310
|
+
} catch {
|
|
4311
|
+
try {
|
|
4312
|
+
child.kill(sig);
|
|
4313
|
+
} catch {}
|
|
4314
|
+
}
|
|
4315
|
+
};
|
|
4316
|
+
const forward = (sig) => () => {
|
|
4317
|
+
killTree(sig);
|
|
4318
|
+
cleanupSignalForwarders();
|
|
4319
|
+
process.kill(process.pid, sig);
|
|
4320
|
+
};
|
|
4321
|
+
const onSigint = forward("SIGINT");
|
|
4322
|
+
const onSighup = forward("SIGHUP");
|
|
4323
|
+
const onSigterm = forward("SIGTERM");
|
|
4324
|
+
const onExit = () => {
|
|
4325
|
+
killTree("SIGKILL");
|
|
4326
|
+
};
|
|
4327
|
+
const cleanupSignalForwarders = () => {
|
|
4328
|
+
process.removeListener("SIGINT", onSigint);
|
|
4329
|
+
process.removeListener("SIGHUP", onSighup);
|
|
4330
|
+
process.removeListener("SIGTERM", onSigterm);
|
|
4331
|
+
};
|
|
4332
|
+
const cleanupForwarders = () => {
|
|
4333
|
+
cleanupSignalForwarders();
|
|
4334
|
+
process.removeListener("exit", onExit);
|
|
4335
|
+
};
|
|
4336
|
+
process.once("SIGINT", onSigint);
|
|
4337
|
+
process.once("SIGHUP", onSighup);
|
|
4338
|
+
process.once("SIGTERM", onSigterm);
|
|
4339
|
+
process.on("exit", onExit);
|
|
4340
|
+
const finish = (result) => {
|
|
4341
|
+
if (settled) {
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4344
|
+
settled = true;
|
|
4345
|
+
for (const t2 of timers) {
|
|
4346
|
+
clearTimeout(t2);
|
|
4347
|
+
}
|
|
4348
|
+
if (!win || timedOut || overflowed || viaExitGrace || result.error !== undefined) {
|
|
4349
|
+
killTree("SIGKILL");
|
|
4350
|
+
}
|
|
4351
|
+
child.stdout?.destroy();
|
|
4352
|
+
cleanupForwarders();
|
|
4353
|
+
resolve2(result);
|
|
4354
|
+
};
|
|
4355
|
+
const schedule = (fn, ms) => {
|
|
4356
|
+
const t2 = setTimeout(fn, ms);
|
|
4357
|
+
t2.unref();
|
|
4358
|
+
timers.push(t2);
|
|
4359
|
+
};
|
|
4360
|
+
schedule(() => {
|
|
4361
|
+
timedOut = true;
|
|
4362
|
+
killTree("SIGTERM");
|
|
4363
|
+
schedule(() => {
|
|
4364
|
+
killTree("SIGKILL");
|
|
4365
|
+
}, 2000);
|
|
4366
|
+
}, options.timeout);
|
|
4367
|
+
child.stdout?.setEncoding("utf8");
|
|
4368
|
+
child.stdout?.on("data", (chunk) => {
|
|
4369
|
+
stdout += chunk;
|
|
4370
|
+
stdoutBytes += Buffer.byteLength(chunk, "utf8");
|
|
4371
|
+
if (!overflowed && stdoutBytes > options.maxBuffer) {
|
|
4372
|
+
overflowed = true;
|
|
4373
|
+
killTree("SIGKILL");
|
|
4374
|
+
}
|
|
4375
|
+
});
|
|
4376
|
+
let viaExitGrace = false;
|
|
4377
|
+
const outcome = (status, signal) => ({
|
|
4378
|
+
...timedOut ? { error: new Error("ETIMEDOUT: LLM backend timed out") } : overflowed ? { error: new Error("LLM backend exceeded the output cap") } : {},
|
|
4379
|
+
signal,
|
|
4380
|
+
status,
|
|
4381
|
+
stdout
|
|
4382
|
+
});
|
|
4383
|
+
child.on("error", (error) => {
|
|
4384
|
+
finish({ error, signal: null, status: null, stdout: null });
|
|
4385
|
+
});
|
|
4386
|
+
child.on("close", (status, signal) => {
|
|
4387
|
+
finish(outcome(status, signal));
|
|
4388
|
+
});
|
|
4389
|
+
child.on("exit", (status, signal) => {
|
|
4390
|
+
schedule(() => {
|
|
4391
|
+
viaExitGrace = true;
|
|
4392
|
+
finish(outcome(status, signal));
|
|
4393
|
+
}, 1000);
|
|
4394
|
+
});
|
|
4395
|
+
child.stdin?.on("error", () => {
|
|
4396
|
+
return;
|
|
4397
|
+
});
|
|
4398
|
+
if (options.input !== undefined) {
|
|
4399
|
+
child.stdin?.write(options.input);
|
|
4400
|
+
}
|
|
4401
|
+
child.stdin?.end();
|
|
4402
|
+
});
|
|
4403
|
+
}
|
|
4404
|
+
function resolveGrouper(opts, deps = {}) {
|
|
4405
|
+
const run = deps.run ?? spawnGroupRun;
|
|
4406
|
+
const whichFn = deps.which ?? which;
|
|
4407
|
+
const cacheDirFn = deps.cacheDir ?? defaultCacheDir;
|
|
4408
|
+
if (opts.cmd !== undefined) {
|
|
4409
|
+
const cmd = opts.cmd;
|
|
4410
|
+
const [shell, shellArgs] = process.platform === "win32" ? ["cmd", ["/d", "/s", "/c", `"${cmd}"`]] : ["sh", ["-c", cmd]];
|
|
4411
|
+
let noticeShown = false;
|
|
4412
|
+
const renderEnvNotice = deps.onEnvCmdRun ?? ((c3) => {
|
|
4413
|
+
console.error(`ccpluginizer: running LLM grouper from CCPLUGINIZER_LLM_CMD: ${c3}`);
|
|
4414
|
+
});
|
|
4415
|
+
const onRun = opts.cmdFromEnv ? () => {
|
|
4416
|
+
if (!noticeShown) {
|
|
4417
|
+
noticeShown = true;
|
|
4418
|
+
renderEnvNotice(cmd);
|
|
4419
|
+
}
|
|
4420
|
+
} : undefined;
|
|
4421
|
+
return {
|
|
4422
|
+
fn: makeCachedGrouper("subprocess", `cmd:${cmd}`, cacheDirFn, (prompt) => run(shell, shellArgs, {
|
|
4423
|
+
input: prompt,
|
|
4424
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
4425
|
+
timeout: opts.timeoutMs
|
|
4426
|
+
}), onRun),
|
|
4427
|
+
backendId: cmd,
|
|
4428
|
+
kind: "subprocess"
|
|
4429
|
+
};
|
|
4430
|
+
}
|
|
4431
|
+
const claude = whichFn("claude");
|
|
4432
|
+
if (claude !== null) {
|
|
4433
|
+
const winShim = process.platform === "win32" && /\.(?:cmd|bat)$/i.test(claude);
|
|
4434
|
+
const [cmd, cmdArgs] = winShim ? ["cmd", ["/d", "/s", "/c", `""${claude}" -p"`]] : [claude, ["-p"]];
|
|
4435
|
+
return {
|
|
4436
|
+
fn: makeCachedGrouper("claude", "claude-cli", cacheDirFn, (prompt) => run(cmd, cmdArgs, {
|
|
4437
|
+
input: prompt,
|
|
4438
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
4439
|
+
timeout: opts.timeoutMs
|
|
4440
|
+
})),
|
|
4441
|
+
backendId: "claude",
|
|
4442
|
+
kind: "claude"
|
|
4443
|
+
};
|
|
4444
|
+
}
|
|
4445
|
+
return null;
|
|
4446
|
+
}
|
|
4447
|
+
function makeCachedGrouper(kind, cacheId, cacheDir, invoke, onRun) {
|
|
4448
|
+
return async (skills) => {
|
|
4449
|
+
const dir = ensureCacheDir(cacheDir);
|
|
4450
|
+
const cacheKey = hashSkills(skills, cacheId);
|
|
4451
|
+
const cached = dir === null ? null : readCacheFile(join11(dir, `${cacheKey}.json`));
|
|
4452
|
+
if (cached !== null) {
|
|
4453
|
+
return { kind, groups: cached };
|
|
4454
|
+
}
|
|
4455
|
+
onRun?.();
|
|
4456
|
+
const result = await invoke(buildClusterPrompt(skills));
|
|
4457
|
+
const stdout = result.stdout;
|
|
4458
|
+
if (isSpawnFailure(result) || stdout === null) {
|
|
4459
|
+
return { kind, groups: [] };
|
|
4460
|
+
}
|
|
4461
|
+
const validDirs = new Set(skills.map((s3) => s3.dir));
|
|
4462
|
+
const groups = parseClusterResponse(stdout, validDirs) ?? [];
|
|
4463
|
+
if (groups.length === 0 || dir === null) {
|
|
4464
|
+
return { kind, groups };
|
|
4465
|
+
}
|
|
4466
|
+
return {
|
|
4467
|
+
kind,
|
|
4468
|
+
groups,
|
|
4469
|
+
commit: () => {
|
|
4470
|
+
writeCacheFile(join11(dir, `${cacheKey}.json`), groups);
|
|
4471
|
+
}
|
|
4472
|
+
};
|
|
4473
|
+
};
|
|
4474
|
+
}
|
|
4475
|
+
function isSpawnFailure(result) {
|
|
4476
|
+
return result.error !== undefined || result.signal !== null || result.status !== null && result.status !== 0;
|
|
4477
|
+
}
|
|
4478
|
+
function which(cmd) {
|
|
4479
|
+
if (typeof Bun !== "undefined") {
|
|
4480
|
+
return Bun.which(cmd);
|
|
4481
|
+
}
|
|
4482
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
4483
|
+
const r2 = spawnSync2(finder, [cmd], { encoding: "utf8" });
|
|
4484
|
+
const out = typeof r2.stdout === "string" ? (r2.stdout.split(`
|
|
4485
|
+
`)[0] ?? "").trim() : "";
|
|
4486
|
+
return r2.status === 0 && out !== "" ? out : null;
|
|
4487
|
+
}
|
|
4488
|
+
function hashSkills(skills, backendId) {
|
|
4489
|
+
const material = JSON.stringify([
|
|
4490
|
+
backendId,
|
|
4491
|
+
skills.map((s3) => JSON.stringify([s3.dir, s3.product ?? "", s3.description])).sort(byCodeUnit)
|
|
4492
|
+
]);
|
|
4493
|
+
return createHash("sha256").update(material).digest("hex").slice(0, 32);
|
|
4494
|
+
}
|
|
4495
|
+
function defaultCacheDir() {
|
|
4496
|
+
return join11(homedir(), ".cache", "ccpluginizer");
|
|
4497
|
+
}
|
|
4498
|
+
function ensureCacheDir(dirFn) {
|
|
4499
|
+
const dir = dirFn();
|
|
4500
|
+
try {
|
|
4501
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
4502
|
+
} catch {
|
|
4503
|
+
return null;
|
|
4504
|
+
}
|
|
4505
|
+
let st;
|
|
4506
|
+
try {
|
|
4507
|
+
st = statSync4(dir);
|
|
4508
|
+
} catch {
|
|
4509
|
+
return null;
|
|
4510
|
+
}
|
|
4511
|
+
if (!st.isDirectory()) {
|
|
4512
|
+
return null;
|
|
4513
|
+
}
|
|
4514
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
4515
|
+
if (uid !== undefined && (st.uid !== uid || (st.mode & 63) !== 0)) {
|
|
4516
|
+
return null;
|
|
4517
|
+
}
|
|
4518
|
+
return dir;
|
|
4519
|
+
}
|
|
4520
|
+
function readCacheFile(file) {
|
|
4521
|
+
try {
|
|
4522
|
+
return validateRawGroups(readJsonFile(file));
|
|
4523
|
+
} catch {
|
|
4524
|
+
return null;
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
function writeCacheFile(file, groups) {
|
|
4528
|
+
try {
|
|
4529
|
+
writeFileSync(file, JSON.stringify(groups), "utf8");
|
|
4530
|
+
} catch {}
|
|
2142
4531
|
}
|
|
2143
4532
|
|
|
2144
4533
|
// src/commands/scan.ts
|
|
2145
|
-
var scanCommand = new R2("scan").meta({ description: "Scan a non-plugin repo and emit a marketplace entry" }).args([{ name: "repo", type: "string", required: true, description: "owner/repo, URL, or local path" }]).flags({
|
|
2146
|
-
output: { type: "string", short: "o", description: "Write entry JSON to file" }
|
|
4534
|
+
var scanCommand = new R2("scan").meta({ description: "Scan a non-plugin repo and emit a marketplace entry (auto-splits bloated plugins)" }).args([{ name: "repo", type: "string", required: true, description: "owner/repo, URL, or local path" }]).flags({
|
|
4535
|
+
output: { type: "string", short: "o", description: "Write the entry/entries JSON to a single file" },
|
|
4536
|
+
outDir: { type: "string", aliases: ["out-dir"], description: "Write one JSON file per entry into this directory" },
|
|
4537
|
+
split: { type: "boolean", default: true, description: "Auto-split bloated plugins (use --no-split to force one entry)" },
|
|
4538
|
+
umbrella: { type: "boolean", default: false, description: "Also emit the everything-in-one umbrella entry" },
|
|
4539
|
+
cluster: { type: "string", default: DEFAULT_STRATEGY, description: "Clustering strategy: auto (deterministic, default) | auto-llm (deterministic, then BYO LLM on no clean partition) | llm (opt-in BYO subprocess/claude) | metadata | directory | name-prefix" },
|
|
4540
|
+
llmCmd: { type: "string", aliases: ["llm-cmd"], description: "BYO LLM grouper command (prompt on stdin, JSON groups on stdout); used by --cluster=llm/auto-llm" },
|
|
4541
|
+
llmTimeout: { type: "number", aliases: ["llm-timeout"], description: "LLM backend timeout in seconds (default 120)" },
|
|
4542
|
+
writeMarker: { type: "boolean", default: false, aliases: ["write-marker"], description: "Freeze the grouping into .ccpluginizer.json" },
|
|
4543
|
+
interactive: { type: "boolean", default: false, description: "Review the proposed split before emitting" },
|
|
4544
|
+
minSkills: { type: "number", default: DEFAULT_MIN_SKILLS_TO_SPLIT, aliases: ["min-skills"], description: "Minimum skill count to attempt a split" }
|
|
2147
4545
|
}).run(async ({ args, flags }) => {
|
|
4546
|
+
const requestedCluster = normalizeStrategy(flags.cluster);
|
|
4547
|
+
const wantSplit = flags.split;
|
|
4548
|
+
if (!wantSplit && flags.umbrella) {
|
|
4549
|
+
console.error("ccpluginizer: --umbrella is ignored with --no-split (the umbrella only exists on the split path).");
|
|
4550
|
+
}
|
|
4551
|
+
const minSkills = Number.isFinite(flags.minSkills) && flags.minSkills >= 0 ? flags.minSkills : DEFAULT_MIN_SKILLS_TO_SPLIT;
|
|
4552
|
+
if (minSkills !== flags.minSkills) {
|
|
4553
|
+
console.error(`ccpluginizer: invalid --min-skills ${String(flags.minSkills)}; using ${String(DEFAULT_MIN_SKILLS_TO_SPLIT)}.`);
|
|
4554
|
+
}
|
|
4555
|
+
const llmConfig = resolveLlmConfig({
|
|
4556
|
+
...flags.llmCmd !== undefined ? { llmCmd: flags.llmCmd } : {},
|
|
4557
|
+
...flags.llmTimeout !== undefined ? { llmTimeout: flags.llmTimeout } : {}
|
|
4558
|
+
});
|
|
4559
|
+
const outputFlags = {
|
|
4560
|
+
output: trimOrUndefined(flags.output),
|
|
4561
|
+
outDir: trimOrUndefined(flags.outDir)
|
|
4562
|
+
};
|
|
4563
|
+
if (outputFlags.outDir !== undefined) {
|
|
4564
|
+
let target = null;
|
|
4565
|
+
try {
|
|
4566
|
+
target = statSync5(outputFlags.outDir);
|
|
4567
|
+
} catch {
|
|
4568
|
+
try {
|
|
4569
|
+
lstatSync(outputFlags.outDir);
|
|
4570
|
+
throw new Error(`--out-dir "${outputFlags.outDir}" is a dangling symlink`);
|
|
4571
|
+
} catch (e3) {
|
|
4572
|
+
if (e3 instanceof Error && e3.message.includes("dangling symlink")) {
|
|
4573
|
+
throw e3;
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
}
|
|
4577
|
+
if (target !== null && !target.isDirectory()) {
|
|
4578
|
+
throw new Error(`--out-dir "${outputFlags.outDir}" exists and is not a directory`);
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
2148
4581
|
const repoPath = await resolveSource(args.repo);
|
|
2149
4582
|
const sourceRepo = inferSourceRepo(args.repo);
|
|
2150
|
-
const
|
|
2151
|
-
|
|
4583
|
+
const group = wantSplit && strategyUsesLlm(requestedCluster) ? makeLazyGrouper(llmConfig) : null;
|
|
4584
|
+
let result = await synthesizeEntries({
|
|
4585
|
+
repoRoot: repoPath,
|
|
4586
|
+
sourceRepo,
|
|
4587
|
+
split: wantSplit,
|
|
4588
|
+
umbrella: flags.umbrella,
|
|
4589
|
+
strategy: requestedCluster,
|
|
4590
|
+
minSkillsToSplit: minSkills,
|
|
4591
|
+
...group !== null ? { group } : {}
|
|
4592
|
+
});
|
|
4593
|
+
const splitCouldHaveUsedLlm = result.provenance.kind !== "skipped" && result.provenance.kind !== "marker";
|
|
4594
|
+
if (wantSplit && !strategyUsesLlm(requestedCluster) && llmConfig.cmd !== undefined && splitCouldHaveUsedLlm) {
|
|
4595
|
+
const source = llmConfig.cmdFromEnv ? "CCPLUGINIZER_LLM_CMD" : "--llm-cmd";
|
|
4596
|
+
console.error(`ccpluginizer: an LLM is configured (${source}) but --cluster=${requestedCluster} is deterministic-only; pass --cluster=llm or --cluster=auto-llm to use it.`);
|
|
4597
|
+
}
|
|
4598
|
+
if (wantSplit && result.existingMarker !== null && markerSuppressesSplit(result.existingMarker) && (requestedCluster !== DEFAULT_STRATEGY || llmConfig.cmd !== undefined)) {
|
|
4599
|
+
console.error('ccpluginizer: the committed .ccpluginizer.json curates a single entry, so --cluster/--llm-cmd were not consulted; remove the marker (or give it "groups") to re-enable splitting.');
|
|
4600
|
+
}
|
|
4601
|
+
const printedWarnings = new Set;
|
|
4602
|
+
const printWarnings = (warnings) => {
|
|
4603
|
+
for (const warning of warnings) {
|
|
4604
|
+
if (!printedWarnings.has(warning)) {
|
|
4605
|
+
printedWarnings.add(warning);
|
|
4606
|
+
console.error(`ccpluginizer: warning: ${warning}`);
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
};
|
|
4610
|
+
if (flags.interactive && result.split !== null) {
|
|
4611
|
+
printWarnings(result.warnings);
|
|
4612
|
+
result = await reviewSplit(result, { repoPath, sourceRepo });
|
|
4613
|
+
}
|
|
4614
|
+
if (result.split !== null) {
|
|
4615
|
+
printSplitNotice(result);
|
|
4616
|
+
} else if (result.provenance.kind === "none") {
|
|
4617
|
+
if (result.provenance.llmFailure !== undefined || requestedCluster !== DEFAULT_STRATEGY) {
|
|
4618
|
+
printNoSplitNotice(requestedCluster, result.provenance.llmFailure);
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
printWarnings(result.warnings);
|
|
4622
|
+
if (flags.writeMarker) {
|
|
4623
|
+
if (result.marker === null) {
|
|
4624
|
+
console.error("ccpluginizer: --write-marker ignored — no split was emitted, so there is no grouping to freeze.");
|
|
4625
|
+
} else if (parseSourceInput(args.repo).kind !== "local") {
|
|
4626
|
+
console.error("ccpluginizer: --write-marker only works on a local path; a github/URL source is cloned to a temp dir that is discarded. Clone the repo locally, re-run `scan <path> --write-marker`, then commit .ccpluginizer.json.");
|
|
4627
|
+
} else {
|
|
4628
|
+
const markerPath = join12(repoPath, ".ccpluginizer.json");
|
|
4629
|
+
const merged = serializeMarkerDraft(result.marker, result.existingMarker);
|
|
4630
|
+
writeFileSync2(markerPath, JSON.stringify(merged, null, 2) + `
|
|
4631
|
+
`, "utf8");
|
|
4632
|
+
console.error(`ccpluginizer: wrote frozen split to ${markerPath}`);
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
emitOutput(result.entries, outputFlags, sourceRepo, result.split !== null);
|
|
4636
|
+
});
|
|
4637
|
+
function normalizeStrategy(value) {
|
|
4638
|
+
if (CLUSTER_STRATEGIES.includes(value)) {
|
|
4639
|
+
return value;
|
|
4640
|
+
}
|
|
4641
|
+
throw new Error(`unknown --cluster "${value}"; expected one of: ${CLUSTER_STRATEGIES.join(", ")}`);
|
|
4642
|
+
}
|
|
4643
|
+
var DEFAULT_TIMEOUT_SECONDS = 120;
|
|
4644
|
+
function trimOrUndefined(value) {
|
|
4645
|
+
if (value === undefined) {
|
|
4646
|
+
return;
|
|
4647
|
+
}
|
|
4648
|
+
const trimmed = value.trim();
|
|
4649
|
+
return trimmed === "" ? undefined : trimmed;
|
|
4650
|
+
}
|
|
4651
|
+
function resolveLlmConfig(flags, env = process.env) {
|
|
4652
|
+
const flagCmd = trimOrUndefined(flags.llmCmd);
|
|
4653
|
+
const envCmd = trimOrUndefined(env["CCPLUGINIZER_LLM_CMD"]);
|
|
4654
|
+
const cmd = flagCmd ?? envCmd;
|
|
4655
|
+
const cmdFromEnv = flagCmd === undefined && envCmd !== undefined;
|
|
4656
|
+
const envTimeout = trimOrUndefined(env["CCPLUGINIZER_LLM_TIMEOUT"]);
|
|
4657
|
+
const resolvedSeconds = flags.llmTimeout ?? (envTimeout !== undefined ? Number(envTimeout) : undefined) ?? DEFAULT_TIMEOUT_SECONDS;
|
|
4658
|
+
const MAX_TIMEOUT_SECONDS = 2147483;
|
|
4659
|
+
const valid = Number.isFinite(resolvedSeconds) && resolvedSeconds > 0;
|
|
4660
|
+
if (!valid && (flags.llmTimeout !== undefined || envTimeout !== undefined)) {
|
|
4661
|
+
console.error(`ccpluginizer: invalid LLM timeout ${String(resolvedSeconds)}; using ${String(DEFAULT_TIMEOUT_SECONDS)}s.`);
|
|
4662
|
+
}
|
|
4663
|
+
const safeSeconds = valid ? Math.min(resolvedSeconds, MAX_TIMEOUT_SECONDS) : DEFAULT_TIMEOUT_SECONDS;
|
|
4664
|
+
return {
|
|
4665
|
+
...cmd !== undefined ? { cmd } : {},
|
|
4666
|
+
cmdFromEnv,
|
|
4667
|
+
timeoutMs: Math.round(safeSeconds * 1000)
|
|
4668
|
+
};
|
|
4669
|
+
}
|
|
4670
|
+
function makeLazyGrouper(config, resolve2 = resolveGrouper) {
|
|
4671
|
+
let backend;
|
|
4672
|
+
return async (skills) => {
|
|
4673
|
+
if (backend === undefined) {
|
|
4674
|
+
backend = resolve2(config);
|
|
4675
|
+
}
|
|
4676
|
+
return backend === null ? null : backend.fn(skills);
|
|
4677
|
+
};
|
|
4678
|
+
}
|
|
4679
|
+
function describeProvenance(provenance) {
|
|
4680
|
+
if (provenance.kind === "marker") {
|
|
4681
|
+
return "via committed marker (.ccpluginizer.json)";
|
|
4682
|
+
}
|
|
4683
|
+
if (provenance.kind === "llm") {
|
|
4684
|
+
return `via ${provenance.backend} clustering`;
|
|
4685
|
+
}
|
|
4686
|
+
return provenance.llmFailure !== undefined ? `via ${provenance.strategy} clustering (${llmFailureReason(provenance.llmFailure)})` : `via ${provenance.strategy} clustering`;
|
|
4687
|
+
}
|
|
4688
|
+
function llmFailureReason(failure) {
|
|
4689
|
+
if (failure.step === "gate-rejected") {
|
|
4690
|
+
return "the LLM grouping was rejected by the acceptance gate";
|
|
4691
|
+
}
|
|
4692
|
+
if (failure.step === "no-output") {
|
|
4693
|
+
return "the LLM backend was unreachable or produced no usable output";
|
|
4694
|
+
}
|
|
4695
|
+
if (failure.step === "errored") {
|
|
4696
|
+
return "the LLM grouper threw an error";
|
|
4697
|
+
}
|
|
4698
|
+
return "no LLM backend found; set --llm-cmd or install the `claude` CLI";
|
|
4699
|
+
}
|
|
4700
|
+
function printSplitNotice(result) {
|
|
4701
|
+
if (result.split === null || result.provenance.kind === "none" || result.provenance.kind === "skipped") {
|
|
4702
|
+
return;
|
|
4703
|
+
}
|
|
4704
|
+
const slices = result.split.groupCount;
|
|
4705
|
+
const parts = [`${String(slices)} skill slice${slices === 1 ? "" : "s"}`];
|
|
4706
|
+
if (result.split.coreEmitted) {
|
|
4707
|
+
parts.push("1 core");
|
|
4708
|
+
}
|
|
4709
|
+
if (result.split.umbrellaEmitted) {
|
|
4710
|
+
parts.push("1 umbrella");
|
|
4711
|
+
}
|
|
4712
|
+
const entryCount = result.entries.length;
|
|
4713
|
+
const via = describeProvenance(result.provenance);
|
|
4714
|
+
console.error(`ccpluginizer: split into ${String(entryCount)} ${entryCount === 1 ? "entry" : "entries"} (${parts.join(" + ")}) ${via}. Use --no-split for a single entry.`);
|
|
4715
|
+
}
|
|
4716
|
+
function printNoSplitNotice(requestedCluster, llmFailure) {
|
|
4717
|
+
const reason = llmFailure !== undefined ? `${llmFailureReason(llmFailure)}, and no clean deterministic partition` : "no clean deterministic partition";
|
|
4718
|
+
console.error(`ccpluginizer: --cluster=${requestedCluster} produced no split — ${reason}; emitting a single entry.`);
|
|
4719
|
+
}
|
|
4720
|
+
async function reviewSplit(result, ctx, confirmFn = (opts) => wJ(opts)) {
|
|
4721
|
+
if (result.split === null || result.provenance.kind === "none" || result.provenance.kind === "skipped") {
|
|
4722
|
+
return result;
|
|
4723
|
+
}
|
|
4724
|
+
console.error(`ccpluginizer: proposed split — ${describeProvenance(result.provenance)}`);
|
|
4725
|
+
for (const g of result.marker?.groups ?? []) {
|
|
4726
|
+
console.error(` ${g.slug}: ${String(g.skills.length)} skills`);
|
|
4727
|
+
}
|
|
4728
|
+
const proceed = await confirmFn({
|
|
4729
|
+
message: `Emit this ${String(result.split.groupCount)}-way split?`,
|
|
4730
|
+
default: true
|
|
4731
|
+
});
|
|
4732
|
+
if (proceed) {
|
|
4733
|
+
return result;
|
|
4734
|
+
}
|
|
4735
|
+
const single = await synthesizeEntries({
|
|
4736
|
+
repoRoot: ctx.repoPath,
|
|
4737
|
+
sourceRepo: ctx.sourceRepo,
|
|
4738
|
+
split: false,
|
|
4739
|
+
existingMarker: result.existingMarker,
|
|
4740
|
+
caches: result.caches
|
|
4741
|
+
});
|
|
4742
|
+
console.error("ccpluginizer: split declined; emitting a single entry.");
|
|
4743
|
+
return single;
|
|
4744
|
+
}
|
|
4745
|
+
function emitOutput(entries, flags, sourceRepo, splitFired) {
|
|
4746
|
+
if (flags.outDir !== undefined) {
|
|
4747
|
+
if (flags.output !== undefined) {
|
|
4748
|
+
console.error("ccpluginizer: both --out-dir and --output given; --output is ignored (writing one file per entry into the directory).");
|
|
4749
|
+
}
|
|
4750
|
+
mkdirSync2(flags.outDir, { recursive: true });
|
|
4751
|
+
for (const entry of entries) {
|
|
4752
|
+
writeFileSync2(join12(flags.outDir, `${entry.name}.json`), JSON.stringify(entry, null, 2) + `
|
|
4753
|
+
`, "utf8");
|
|
4754
|
+
}
|
|
4755
|
+
warnAboutStaleEntries(entries, flags.outDir, sourceRepo);
|
|
4756
|
+
console.error(`ccpluginizer: wrote ${String(entries.length)} entr${entries.length === 1 ? "y" : "ies"} to ${flags.outDir}`);
|
|
4757
|
+
return;
|
|
4758
|
+
}
|
|
4759
|
+
const payload = !splitFired && entries.length === 1 ? entries[0] : entries;
|
|
4760
|
+
const json = JSON.stringify(payload, null, 2);
|
|
2152
4761
|
if (flags.output !== undefined) {
|
|
2153
|
-
|
|
4762
|
+
writeFileSync2(flags.output, json + `
|
|
2154
4763
|
`, "utf8");
|
|
2155
4764
|
} else {
|
|
2156
4765
|
console.log(json);
|
|
2157
4766
|
}
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
});
|
|
2184
|
-
|
|
2185
|
-
var MarketplaceEntrySchema = object({
|
|
2186
|
-
name: pipe(string(), regex(/^[a-z0-9][a-z0-9-]*$/)),
|
|
2187
|
-
source: SourceSchema,
|
|
2188
|
-
strict: optional(boolean()),
|
|
2189
|
-
description: optional(string()),
|
|
2190
|
-
version: optional(string()),
|
|
2191
|
-
author: optional(union([string(), object({ name: string() })])),
|
|
2192
|
-
homepage: optional(string()),
|
|
2193
|
-
repository: optional(string()),
|
|
2194
|
-
license: optional(string()),
|
|
2195
|
-
keywords: optional(array(string())),
|
|
2196
|
-
skills: optional(array(PathString)),
|
|
2197
|
-
agents: optional(array(PathString)),
|
|
2198
|
-
commands: optional(array(PathString)),
|
|
2199
|
-
hooks: optional(union([PathString, record(string(), unknown())])),
|
|
2200
|
-
mcpServers: optional(union([PathString, record(string(), unknown())])),
|
|
2201
|
-
outputStyles: optional(array(PathString)),
|
|
2202
|
-
themes: optional(array(PathString)),
|
|
2203
|
-
monitors: optional(PathString)
|
|
2204
|
-
});
|
|
4767
|
+
}
|
|
4768
|
+
function warnAboutStaleEntries(entries, outDir, sourceRepo) {
|
|
4769
|
+
if (sourceRepo.startsWith("local/")) {
|
|
4770
|
+
return;
|
|
4771
|
+
}
|
|
4772
|
+
const current = new Set(entries.map((e3) => `${e3.name}.json`));
|
|
4773
|
+
const expectedUrl = sourceRepoUrl(sourceRepo);
|
|
4774
|
+
const stale = readdirSync4(outDir).filter((f2) => f2.endsWith(".json") && !current.has(f2)).filter((f2) => {
|
|
4775
|
+
try {
|
|
4776
|
+
return entryReferencesUrl(readJsonFile(join12(outDir, f2)), expectedUrl);
|
|
4777
|
+
} catch {
|
|
4778
|
+
return false;
|
|
4779
|
+
}
|
|
4780
|
+
}).sort();
|
|
4781
|
+
if (stale.length > 0) {
|
|
4782
|
+
console.error(`ccpluginizer: warning: ${String(stale.length)} entry file(s) from a previous scan of this repo remain in ${outDir}: ${stale.join(", ")}. Delete them if this regrouping replaced them.`);
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
function entryReferencesUrl(parsed, url) {
|
|
4786
|
+
return toEntryList(parsed).some((item) => {
|
|
4787
|
+
if (item === null || typeof item !== "object") {
|
|
4788
|
+
return false;
|
|
4789
|
+
}
|
|
4790
|
+
const source = item.source;
|
|
4791
|
+
return source !== null && typeof source === "object" && source.url === url;
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
2205
4794
|
|
|
2206
4795
|
// src/commands/validate.ts
|
|
2207
|
-
var validateCommand = new R2("validate").meta({ description: "Validate
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
const
|
|
2211
|
-
|
|
4796
|
+
var validateCommand = new R2("validate").meta({ description: "Validate marketplace entries against the schema (file, JSON array, or directory)" }).args([
|
|
4797
|
+
{ name: "entryFile", type: "string", required: true, description: "Path to an entry JSON file, a JSON array, or a directory of entries" }
|
|
4798
|
+
]).run(({ args }) => {
|
|
4799
|
+
const { items, sources } = collectEntries(args.entryFile);
|
|
4800
|
+
const result = validateEntries(items, sources);
|
|
4801
|
+
if (!result.ok) {
|
|
2212
4802
|
console.error("Validation failed:");
|
|
2213
|
-
for (const
|
|
2214
|
-
console.error(
|
|
4803
|
+
for (const error of result.errors) {
|
|
4804
|
+
console.error(` - ${error}`);
|
|
2215
4805
|
}
|
|
2216
4806
|
process.exit(1);
|
|
2217
4807
|
}
|
|
2218
|
-
console.log(
|
|
4808
|
+
console.log(`OK (${String(items.length)} ${items.length === 1 ? "entry" : "entries"})`);
|
|
2219
4809
|
});
|
|
2220
4810
|
|
|
2221
4811
|
// src/index.ts
|