@ccpluginizer/ccpluginizer 0.5.0 → 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.
Files changed (2) hide show
  1. package/dist/index.js +2795 -205
  2. 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(/^[a-z0-9][a-z0-9-]*$/)),
2434
+ name: pipe(string(), regex(NAME_REGEX)),
1690
2435
  description: optional(string()),
1691
- skills: optional(array(pipe(string(), startsWith("./")))),
1692
- agents: optional(array(pipe(string(), startsWith("./")))),
1693
- commands: optional(array(pipe(string(), startsWith("./")))),
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(pipe(string(), startsWith("./")))),
1697
- themes: optional(array(pipe(string(), startsWith("./")))),
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 = join3(repoRoot, ".ccpluginizer.json");
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 = JSON.parse(raw);
1714
- } catch (e) {
1715
- throw new MarkerFileError(`Invalid JSON in ${markerPath}: ${e instanceof Error ? e.message : String(e)}`, []);
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 { existsSync as existsSync3, readdirSync, statSync } from "node:fs";
1726
- import { join as join4 } from "node:path";
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: "commands", kind: "commands", emit: "directory" },
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 rootFindings = scanRoot(repoRoot, "");
1741
- const dotfilesFindings = scanRoot(repoRoot, ".claude");
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 : join4(repoRoot, prefix);
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
- const folderPath = join4(baseDir, folder);
1750
- if (!existsSync3(folderPath) || !statSync(folderPath).isDirectory()) {
2520
+ if (!dirContainsDir(list, baseDir, folder)) {
1751
2521
  continue;
1752
2522
  }
1753
- const entries = readdirSync(folderPath);
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((e) => e.endsWith(emit.enumerateFiles));
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((f) => `${pathPrefix}${folder}/${f}`),
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
- if (existsSync3(join4(baseDir, file))) {
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 existsSync4, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
1807
- import { join as join5 } from "node:path";
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 = join5(repoRoot, ".claude-plugin");
1828
- if (!existsSync4(dir) || !statSync2(dir).isDirectory()) {
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 = join5(dir, entry);
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 m = result.output;
1850
- const hasComponent = m.skills !== undefined || m.agents !== undefined || m.commands !== undefined || m.hooks !== undefined || m.mcpServers !== undefined;
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: m };
2628
+ return { filename: entry, manifest: m2 };
1855
2629
  }
1856
2630
  return null;
1857
2631
  }
1858
2632
 
1859
2633
  // src/detector/contentSniff.ts
1860
- import { readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "node:fs";
1861
- import { join as join6, dirname, relative } from "node:path";
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(string()),
1866
- description: string(),
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: string(),
1871
- description: string(),
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/contentSniff.ts
1880
- function detectContentSniff(repoRoot) {
1881
- const skillDirs = new Set;
1882
- const agentFiles = new Set;
1883
- walk(repoRoot, repoRoot, (filePath) => {
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 yamlBody = match[1];
1939
- if (yamlBody === undefined) {
2670
+ const body = match[1];
2671
+ if (body === undefined) {
1940
2672
  return null;
1941
2673
  }
1942
- return parseYamlBlock(yamlBody);
2674
+ return parseYamlFrontmatter(body);
1943
2675
  }
1944
- function parseYamlBlock(body) {
1945
- const out = {};
1946
- for (const line of body.split(`
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
- out[key] = coerceYamlValue(rawValue);
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
- return Number(raw);
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 existsSync5 } from "node:fs";
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 (existsSync5(fullPath)) {
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
- function synthesizeEntry(input) {
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 marker = detectMarkerFile(input.repoRoot);
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
- const conventionFindings = detectConventions(input.repoRoot);
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((f) => {
2027
- if (f.source === "convention" && manifestKindsWithFindings.has(f.kind)) {
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.description !== undefined ? { description: marker.description } : {},
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((f) => f.paths);
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 f of findings) {
2122
- if (f.kind === kind) {
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 f of findings) {
2131
- const list = map.get(f.kind) ?? [];
2132
- list.push(f);
2133
- map.set(f.kind, list);
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: `https://github.com/${repo}.git` };
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.replace("/", "-").toLowerCase();
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 entry = synthesizeEntry({ repoRoot: repoPath, sourceRepo });
2151
- const json = JSON.stringify(entry, null, 2);
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
- writeFileSync(flags.output, json + `
4762
+ writeFileSync2(flags.output, json + `
2154
4763
  `, "utf8");
2155
4764
  } else {
2156
4765
  console.log(json);
2157
4766
  }
2158
- });
2159
-
2160
- // src/commands/validate.ts
2161
- import { readFileSync as readFileSync4 } from "node:fs";
2162
-
2163
- // src/schemas/marketplaceEntry.ts
2164
- var PathString = pipe(string(), startsWith("./"));
2165
- var GithubSourceSchema = strictObject({
2166
- source: literal("github"),
2167
- repo: pipe(string(), regex(/^[\w.-]+\/[\w.-]+$/)),
2168
- ref: optional(string()),
2169
- sha: optional(string())
2170
- });
2171
- var UrlSourceSchema = strictObject({
2172
- source: literal("url"),
2173
- url: string(),
2174
- ref: optional(string()),
2175
- sha: optional(string())
2176
- });
2177
- var GitSubdirSourceSchema = strictObject({
2178
- source: literal("git-subdir"),
2179
- url: string(),
2180
- path: string(),
2181
- ref: optional(string()),
2182
- sha: optional(string())
2183
- });
2184
- var SourceSchema = union([GithubSourceSchema, UrlSourceSchema, GitSubdirSourceSchema]);
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 a marketplace entry JSON file against the schema" }).args([{ name: "entryFile", type: "string", required: true, description: "Path to entry JSON file" }]).run(({ args }) => {
2208
- const raw = readFileSync4(args.entryFile, "utf8");
2209
- const parsed = JSON.parse(raw);
2210
- const result = safeParse(MarketplaceEntrySchema, parsed);
2211
- if (!result.success) {
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 issue of result.issues) {
2214
- console.error(JSON.stringify(issue));
4803
+ for (const error of result.errors) {
4804
+ console.error(` - ${error}`);
2215
4805
  }
2216
4806
  process.exit(1);
2217
4807
  }
2218
- console.log("OK");
4808
+ console.log(`OK (${String(items.length)} ${items.length === 1 ? "entry" : "entries"})`);
2219
4809
  });
2220
4810
 
2221
4811
  // src/index.ts