rulesync 8.30.1 → 8.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/dist/{chunk-DF7KWJTN.js → chunk-3SMTK3M3.js} +7318 -3165
- package/dist/cli/index.cjs +9026 -4652
- package/dist/cli/index.js +1337 -1092
- package/dist/index.cjs +7317 -3176
- package/dist/index.d.cts +52 -5
- package/dist/index.d.ts +52 -5
- package/dist/index.js +1 -1
- package/package.json +16 -13
package/dist/cli/index.js
CHANGED
|
@@ -4,28 +4,29 @@ import {
|
|
|
4
4
|
ALL_FEATURES_WITH_WILDCARD,
|
|
5
5
|
ALL_TOOL_TARGETS,
|
|
6
6
|
ALL_TOOL_TARGETS_WITH_WILDCARD,
|
|
7
|
-
CLAUDECODE_AGENTS_DIR_PATH,
|
|
8
|
-
CLAUDECODE_COMMANDS_DIR_PATH,
|
|
9
7
|
CLAUDECODE_DIR,
|
|
10
8
|
CLAUDECODE_LOCAL_RULE_FILE_NAME,
|
|
11
|
-
CLAUDECODE_MCP_FILE_NAME,
|
|
12
9
|
CLAUDECODE_MEMORIES_DIR_NAME,
|
|
13
|
-
CLAUDECODE_RULES_DIR_NAME,
|
|
14
|
-
CLAUDECODE_RULE_FILE_NAME,
|
|
15
10
|
CLAUDECODE_SETTINGS_LOCAL_FILE_NAME,
|
|
16
11
|
CLAUDECODE_SKILLS_DIR_PATH,
|
|
17
12
|
CLIError,
|
|
18
13
|
CommandsProcessor,
|
|
14
|
+
CommandsProcessorToolTargetSchema,
|
|
19
15
|
ConfigResolver,
|
|
20
16
|
ConsoleLogger,
|
|
21
17
|
ErrorCodes,
|
|
22
18
|
FETCH_CONCURRENCY_LIMIT,
|
|
23
19
|
GITIGNORE_DESTINATION_KEY,
|
|
24
20
|
HooksProcessor,
|
|
21
|
+
HooksProcessorToolTargetSchema,
|
|
25
22
|
IgnoreProcessor,
|
|
23
|
+
IgnoreProcessorToolTargetSchema,
|
|
26
24
|
JsonLogger,
|
|
27
25
|
MAX_FILE_SIZE,
|
|
28
26
|
McpProcessor,
|
|
27
|
+
McpProcessorToolTargetSchema,
|
|
28
|
+
PermissionsProcessor,
|
|
29
|
+
PermissionsProcessorToolTargetSchema,
|
|
29
30
|
RULESYNC_AIIGNORE_FILE_NAME,
|
|
30
31
|
RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
|
|
31
32
|
RULESYNC_COMMANDS_RELATIVE_DIR_PATH,
|
|
@@ -48,6 +49,7 @@ import {
|
|
|
48
49
|
RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH,
|
|
49
50
|
RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
50
51
|
RulesProcessor,
|
|
52
|
+
RulesProcessorToolTargetSchema,
|
|
51
53
|
RulesyncCommand,
|
|
52
54
|
RulesyncCommandFrontmatterSchema,
|
|
53
55
|
RulesyncHooks,
|
|
@@ -62,7 +64,9 @@ import {
|
|
|
62
64
|
RulesyncSubagentFrontmatterSchema,
|
|
63
65
|
SKILL_FILE_NAME,
|
|
64
66
|
SkillsProcessor,
|
|
67
|
+
SkillsProcessorToolTargetSchema,
|
|
65
68
|
SubagentsProcessor,
|
|
69
|
+
SubagentsProcessorToolTargetSchema,
|
|
66
70
|
ToolTargetSchema,
|
|
67
71
|
checkPathTraversal,
|
|
68
72
|
checkRulesyncDirExists,
|
|
@@ -88,8 +92,16 @@ import {
|
|
|
88
92
|
removeTempDirectory,
|
|
89
93
|
stringifyFrontmatter,
|
|
90
94
|
toPosixPath,
|
|
95
|
+
toolCommandFactories,
|
|
96
|
+
toolHooksFactories,
|
|
97
|
+
toolIgnoreFactories,
|
|
98
|
+
toolMcpFactories,
|
|
99
|
+
toolPermissionsFactories,
|
|
100
|
+
toolRuleFactories,
|
|
101
|
+
toolSkillFactories,
|
|
102
|
+
toolSubagentFactories,
|
|
91
103
|
writeFileContent
|
|
92
|
-
} from "../chunk-
|
|
104
|
+
} from "../chunk-3SMTK3M3.js";
|
|
93
105
|
|
|
94
106
|
// src/cli/index.ts
|
|
95
107
|
import { Command } from "commander";
|
|
@@ -1142,8 +1154,11 @@ function logFeatureResult(logger5, params) {
|
|
|
1142
1154
|
}
|
|
1143
1155
|
}
|
|
1144
1156
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1157
|
+
function resolveOutputRoots({
|
|
1158
|
+
logger: logger5,
|
|
1159
|
+
baseDir,
|
|
1160
|
+
outputRoots
|
|
1161
|
+
}) {
|
|
1147
1162
|
if (baseDir !== void 0) {
|
|
1148
1163
|
logger5.warn(
|
|
1149
1164
|
"--base-dir is deprecated; use --output-roots instead. It will be removed in a future major release."
|
|
@@ -1155,6 +1170,53 @@ async function generateCommand(logger5, options) {
|
|
|
1155
1170
|
`Both '--output-roots' and '--base-dir' were provided with differing values; using '--output-roots' (${JSON.stringify(outputRoots)}) and ignoring '--base-dir' (${JSON.stringify(baseDir)}).`
|
|
1156
1171
|
);
|
|
1157
1172
|
}
|
|
1173
|
+
return outputRootsResolved;
|
|
1174
|
+
}
|
|
1175
|
+
var FEATURE_DEBUG_MESSAGES = {
|
|
1176
|
+
ignore: "Generating ignore files...",
|
|
1177
|
+
mcp: "Generating MCP files...",
|
|
1178
|
+
commands: "Generating command files...",
|
|
1179
|
+
subagents: "Generating subagent files...",
|
|
1180
|
+
skills: "Generating skill files...",
|
|
1181
|
+
hooks: "Generating hooks...",
|
|
1182
|
+
rules: "Generating rule files..."
|
|
1183
|
+
};
|
|
1184
|
+
var FEATURE_DEBUG_ORDER = [
|
|
1185
|
+
"ignore",
|
|
1186
|
+
"mcp",
|
|
1187
|
+
"commands",
|
|
1188
|
+
"subagents",
|
|
1189
|
+
"skills",
|
|
1190
|
+
"hooks",
|
|
1191
|
+
"rules"
|
|
1192
|
+
];
|
|
1193
|
+
function logFeatureDebugMessages(logger5, features) {
|
|
1194
|
+
for (const feature of FEATURE_DEBUG_ORDER) {
|
|
1195
|
+
if (features.includes(feature)) {
|
|
1196
|
+
logger5.debug(FEATURE_DEBUG_MESSAGES[feature] ?? "");
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
function buildSummaryParts(result) {
|
|
1201
|
+
const summarySpecs = [
|
|
1202
|
+
{ count: result.rulesCount, label: "rules" },
|
|
1203
|
+
{ count: result.ignoreCount, label: "ignore files" },
|
|
1204
|
+
{ count: result.mcpCount, label: "MCP files" },
|
|
1205
|
+
{ count: result.commandsCount, label: "commands" },
|
|
1206
|
+
{ count: result.subagentsCount, label: "subagents" },
|
|
1207
|
+
{ count: result.skillsCount, label: "skills" },
|
|
1208
|
+
{ count: result.hooksCount, label: "hooks" },
|
|
1209
|
+
{ count: result.permissionsCount, label: "permissions" }
|
|
1210
|
+
];
|
|
1211
|
+
const parts = [];
|
|
1212
|
+
for (const { count, label } of summarySpecs) {
|
|
1213
|
+
if (count > 0) parts.push(`${count} ${label}`);
|
|
1214
|
+
}
|
|
1215
|
+
return parts;
|
|
1216
|
+
}
|
|
1217
|
+
async function generateCommand(logger5, options) {
|
|
1218
|
+
const { baseDir, outputRoots, ...rest } = options;
|
|
1219
|
+
const outputRootsResolved = resolveOutputRoots({ logger: logger5, baseDir, outputRoots });
|
|
1158
1220
|
const config = await ConfigResolver.resolve(
|
|
1159
1221
|
{ ...rest, outputRoots: outputRootsResolved },
|
|
1160
1222
|
{ logger: logger5 }
|
|
@@ -1171,27 +1233,7 @@ async function generateCommand(logger5, options) {
|
|
|
1171
1233
|
}
|
|
1172
1234
|
logger5.debug(`Output roots: ${config.getOutputRoots().join(", ")}`);
|
|
1173
1235
|
const features = config.getFeatures();
|
|
1174
|
-
|
|
1175
|
-
logger5.debug("Generating ignore files...");
|
|
1176
|
-
}
|
|
1177
|
-
if (features.includes("mcp")) {
|
|
1178
|
-
logger5.debug("Generating MCP files...");
|
|
1179
|
-
}
|
|
1180
|
-
if (features.includes("commands")) {
|
|
1181
|
-
logger5.debug("Generating command files...");
|
|
1182
|
-
}
|
|
1183
|
-
if (features.includes("subagents")) {
|
|
1184
|
-
logger5.debug("Generating subagent files...");
|
|
1185
|
-
}
|
|
1186
|
-
if (features.includes("skills")) {
|
|
1187
|
-
logger5.debug("Generating skill files...");
|
|
1188
|
-
}
|
|
1189
|
-
if (features.includes("hooks")) {
|
|
1190
|
-
logger5.debug("Generating hooks...");
|
|
1191
|
-
}
|
|
1192
|
-
if (features.includes("rules")) {
|
|
1193
|
-
logger5.debug("Generating rule files...");
|
|
1194
|
-
}
|
|
1236
|
+
logFeatureDebugMessages(logger5, features);
|
|
1195
1237
|
const result = await generate({ config, logger: logger5 });
|
|
1196
1238
|
const totalGenerated = calculateTotalCount(result);
|
|
1197
1239
|
const featureResults = {
|
|
@@ -1244,15 +1286,7 @@ async function generateCommand(logger5, options) {
|
|
|
1244
1286
|
logger5.info(`\u2713 All files are up to date (${enabledFeatures})`);
|
|
1245
1287
|
return;
|
|
1246
1288
|
}
|
|
1247
|
-
const parts =
|
|
1248
|
-
if (result.rulesCount > 0) parts.push(`${result.rulesCount} rules`);
|
|
1249
|
-
if (result.ignoreCount > 0) parts.push(`${result.ignoreCount} ignore files`);
|
|
1250
|
-
if (result.mcpCount > 0) parts.push(`${result.mcpCount} MCP files`);
|
|
1251
|
-
if (result.commandsCount > 0) parts.push(`${result.commandsCount} commands`);
|
|
1252
|
-
if (result.subagentsCount > 0) parts.push(`${result.subagentsCount} subagents`);
|
|
1253
|
-
if (result.skillsCount > 0) parts.push(`${result.skillsCount} skills`);
|
|
1254
|
-
if (result.hooksCount > 0) parts.push(`${result.hooksCount} hooks`);
|
|
1255
|
-
if (result.permissionsCount > 0) parts.push(`${result.permissionsCount} permissions`);
|
|
1289
|
+
const parts = buildSummaryParts(result);
|
|
1256
1290
|
if (isPreview) {
|
|
1257
1291
|
logger5.info(`${modePrefix} Would write ${totalGenerated} file(s) total (${parts.join(" + ")})`);
|
|
1258
1292
|
} else {
|
|
@@ -1263,371 +1297,230 @@ async function generateCommand(logger5, options) {
|
|
|
1263
1297
|
// src/cli/commands/gitignore.ts
|
|
1264
1298
|
import { join as join2 } from "path";
|
|
1265
1299
|
|
|
1266
|
-
// src/
|
|
1267
|
-
var
|
|
1268
|
-
return typeof target === "string" ? [target] : target;
|
|
1269
|
-
};
|
|
1270
|
-
var GITIGNORE_ENTRY_REGISTRY = [
|
|
1271
|
-
// Common / general
|
|
1272
|
-
{
|
|
1273
|
-
target: "common",
|
|
1274
|
-
feature: "general",
|
|
1275
|
-
entry: `${RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH}/`
|
|
1276
|
-
},
|
|
1277
|
-
{ target: "common", feature: "general", entry: ".rulesync/rules/*.local.md" },
|
|
1278
|
-
{ target: "common", feature: "general", entry: "rulesync.local.jsonc" },
|
|
1279
|
-
// AGENTS.local.md is placed in common scope (not rovodev-only) so that
|
|
1280
|
-
// local rule files are always gitignored regardless of which targets are enabled.
|
|
1281
|
-
// This prevents accidental commits when a user disables the rovodev target.
|
|
1282
|
-
{ target: "common", feature: "general", entry: "**/AGENTS.local.md" },
|
|
1283
|
-
// AGENTS.md
|
|
1284
|
-
{
|
|
1285
|
-
target: ["agentsmd", "amp", "antigravity-cli", "antigravity-ide", "pi", "vibe", "warp"],
|
|
1286
|
-
feature: "rules",
|
|
1287
|
-
entry: "**/AGENTS.md"
|
|
1288
|
-
},
|
|
1289
|
-
{ target: "agentsmd", feature: "rules", entry: "**/.agents/" },
|
|
1290
|
-
// Amp
|
|
1291
|
-
// Amp reads project rules from a root `AGENTS.md` (handled by the shared
|
|
1292
|
-
// AGENTS.md entry above) plus `.agents/memories/*.md` non-root rules. Agent
|
|
1293
|
-
// Skills come from the shared `.agents/skills/` project tree. The
|
|
1294
|
-
// `.amp/settings.json` permissions file is a user-managed file (like other
|
|
1295
|
-
// tools' settings.json), so it is intentionally not gitignored.
|
|
1296
|
-
{ target: "amp", feature: "rules", entry: "**/.agents/memories/" },
|
|
1297
|
-
{ target: "amp", feature: "skills", entry: "**/.agents/skills/" },
|
|
1298
|
-
// Antigravity (IDE + CLI, Antigravity 2.0)
|
|
1299
|
-
// Both targets share the `.agents/` project tree and both write a root
|
|
1300
|
-
// `AGENTS.md` (handled by the shared AGENTS.md entry above). Global-scope
|
|
1301
|
-
// paths (under the home directory, e.g. `~/.gemini/GEMINI.md`) are
|
|
1302
|
-
// intentionally not gitignored.
|
|
1300
|
+
// src/types/processor-registry.ts
|
|
1301
|
+
var PROCESSOR_REGISTRY = [
|
|
1303
1302
|
{
|
|
1304
|
-
target: ["antigravity-ide", "antigravity-cli"],
|
|
1305
1303
|
feature: "rules",
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
{
|
|
1310
|
-
target: "antigravity-ide",
|
|
1311
|
-
feature: "commands",
|
|
1312
|
-
entry: "**/.agents/workflows/"
|
|
1304
|
+
processor: RulesProcessor,
|
|
1305
|
+
schema: RulesProcessorToolTargetSchema,
|
|
1306
|
+
factory: toolRuleFactories
|
|
1313
1307
|
},
|
|
1314
1308
|
{
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1309
|
+
feature: "ignore",
|
|
1310
|
+
processor: IgnoreProcessor,
|
|
1311
|
+
schema: IgnoreProcessorToolTargetSchema,
|
|
1312
|
+
factory: toolIgnoreFactories
|
|
1318
1313
|
},
|
|
1319
1314
|
{
|
|
1320
|
-
target: ["antigravity-ide", "antigravity-cli"],
|
|
1321
1315
|
feature: "mcp",
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
target: ["antigravity-ide", "antigravity-cli"],
|
|
1326
|
-
feature: "hooks",
|
|
1327
|
-
entry: "**/.agents/hooks.json"
|
|
1328
|
-
},
|
|
1329
|
-
// Augment Code
|
|
1330
|
-
{ target: "augmentcode", feature: "rules", entry: "**/.augment/rules/" },
|
|
1331
|
-
{ target: "augmentcode", feature: "rules", entry: "**/.augment-guidelines" },
|
|
1332
|
-
{ target: "augmentcode", feature: "commands", entry: "**/.augment/commands/" },
|
|
1333
|
-
{ target: "augmentcode", feature: "subagents", entry: "**/.augment/agents/" },
|
|
1334
|
-
{ target: "augmentcode", feature: "skills", entry: "**/.augment/skills/" },
|
|
1335
|
-
{ target: "augmentcode", feature: "ignore", entry: "**/.augmentignore" },
|
|
1336
|
-
{ target: "augmentcode", feature: "permissions", entry: "**/.augment/settings.json" },
|
|
1337
|
-
{ target: "augmentcode", feature: "hooks", entry: "**/.augment/settings.json" },
|
|
1338
|
-
// settings.json is shared with permissions/hooks; re-tag it under `mcp` so
|
|
1339
|
-
// target+feature filtering still resolves the file for MCP runs.
|
|
1340
|
-
{ target: "augmentcode", feature: "mcp", entry: "**/.augment/settings.json" },
|
|
1341
|
-
// Claude Code
|
|
1342
|
-
{ target: "claudecode", feature: "rules", entry: `**/${CLAUDECODE_RULE_FILE_NAME}` },
|
|
1343
|
-
{ target: "claudecode", feature: "rules", entry: `**/${CLAUDECODE_LOCAL_RULE_FILE_NAME}` },
|
|
1344
|
-
{
|
|
1345
|
-
target: "claudecode",
|
|
1346
|
-
feature: "rules",
|
|
1347
|
-
entry: `**/${CLAUDECODE_DIR}/${CLAUDECODE_RULE_FILE_NAME}`
|
|
1348
|
-
},
|
|
1349
|
-
{
|
|
1350
|
-
target: "claudecode",
|
|
1351
|
-
feature: "rules",
|
|
1352
|
-
entry: `**/${CLAUDECODE_DIR}/${CLAUDECODE_LOCAL_RULE_FILE_NAME}`
|
|
1353
|
-
},
|
|
1354
|
-
{
|
|
1355
|
-
target: "claudecode",
|
|
1356
|
-
feature: "rules",
|
|
1357
|
-
entry: `**/${CLAUDECODE_DIR}/${CLAUDECODE_RULES_DIR_NAME}/`
|
|
1316
|
+
processor: McpProcessor,
|
|
1317
|
+
schema: McpProcessorToolTargetSchema,
|
|
1318
|
+
factory: toolMcpFactories
|
|
1358
1319
|
},
|
|
1359
1320
|
{
|
|
1360
|
-
target: "claudecode",
|
|
1361
1321
|
feature: "commands",
|
|
1362
|
-
|
|
1322
|
+
processor: CommandsProcessor,
|
|
1323
|
+
schema: CommandsProcessorToolTargetSchema,
|
|
1324
|
+
factory: toolCommandFactories
|
|
1363
1325
|
},
|
|
1364
1326
|
{
|
|
1365
|
-
target: "claudecode",
|
|
1366
1327
|
feature: "subagents",
|
|
1367
|
-
|
|
1328
|
+
processor: SubagentsProcessor,
|
|
1329
|
+
schema: SubagentsProcessorToolTargetSchema,
|
|
1330
|
+
factory: toolSubagentFactories
|
|
1368
1331
|
},
|
|
1369
1332
|
{
|
|
1370
|
-
target: "claudecode",
|
|
1371
1333
|
feature: "skills",
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
{
|
|
1376
|
-
target: "claudecode",
|
|
1377
|
-
feature: "general",
|
|
1378
|
-
entry: `**/${CLAUDECODE_DIR}/${CLAUDECODE_MEMORIES_DIR_NAME}/`
|
|
1334
|
+
processor: SkillsProcessor,
|
|
1335
|
+
schema: SkillsProcessorToolTargetSchema,
|
|
1336
|
+
factory: toolSkillFactories
|
|
1379
1337
|
},
|
|
1380
1338
|
{
|
|
1381
|
-
target: "claudecode",
|
|
1382
|
-
feature: "general",
|
|
1383
|
-
entry: `**/${CLAUDECODE_DIR}/${CLAUDECODE_SETTINGS_LOCAL_FILE_NAME}`
|
|
1384
|
-
},
|
|
1385
|
-
{ target: "claudecode", feature: "general", entry: `**/${CLAUDECODE_DIR}/*.lock` },
|
|
1386
|
-
// Cline
|
|
1387
|
-
{ target: "cline", feature: "rules", entry: "**/.clinerules/" },
|
|
1388
|
-
{ target: "cline", feature: "commands", entry: "**/.clinerules/workflows/" },
|
|
1389
|
-
{ target: "cline", feature: "ignore", entry: "**/.clineignore" },
|
|
1390
|
-
// Cline reads MCP only from a global settings file
|
|
1391
|
-
// (`~/.cline/data/settings/cline_mcp_settings.json`), which lives under the
|
|
1392
|
-
// home directory and is not gitignored at the project level.
|
|
1393
|
-
{ target: "cline", feature: "subagents", entry: "**/.cline/agents/" },
|
|
1394
|
-
{ target: "cline", feature: "permissions", entry: "**/.cline/command-permissions.json" },
|
|
1395
|
-
// Codex CLI
|
|
1396
|
-
{ target: "codexcli", feature: "ignore", entry: "**/.codexignore" },
|
|
1397
|
-
{ target: "codexcli", feature: "skills", entry: "**/.agents/skills/" },
|
|
1398
|
-
{ target: "codexcli", feature: "subagents", entry: "**/.codex/agents/" },
|
|
1399
|
-
{ target: "codexcli", feature: "general", entry: "**/.codex/memories/" },
|
|
1400
|
-
{ target: "codexcli", feature: "general", entry: "**/.codex/config.toml" },
|
|
1401
|
-
{ target: "codexcli", feature: "hooks", entry: "**/.codex/hooks.json" },
|
|
1402
|
-
// Cursor
|
|
1403
|
-
{ target: "cursor", feature: "rules", entry: "**/.cursor/" },
|
|
1404
|
-
{ target: "cursor", feature: "ignore", entry: "**/.cursorignore" },
|
|
1405
|
-
// .cursor/cli.json (project) and .cursor/cli-config.json (global) are
|
|
1406
|
-
// already covered by the broader **/.cursor/ entry above; the additional
|
|
1407
|
-
// `permissions` and `hooks` tags below register the same prefix under the
|
|
1408
|
-
// matching feature so target+feature filtering still resolves correctly.
|
|
1409
|
-
{ target: "cursor", feature: "permissions", entry: "**/.cursor/" },
|
|
1410
|
-
{ target: "cursor", feature: "hooks", entry: "**/.cursor/" },
|
|
1411
|
-
// deepagents-cli
|
|
1412
|
-
// All rule content is folded into the single `.deepagents/AGENTS.md`; there is
|
|
1413
|
-
// no `.deepagents/memories/` directory.
|
|
1414
|
-
{ target: "deepagents", feature: "rules", entry: "**/.deepagents/AGENTS.md" },
|
|
1415
|
-
{ target: "deepagents", feature: "mcp", entry: "**/.deepagents/.mcp.json" },
|
|
1416
|
-
{ target: "deepagents", feature: "skills", entry: "**/.deepagents/skills/" },
|
|
1417
|
-
{
|
|
1418
|
-
target: "deepagents",
|
|
1419
|
-
feature: "subagents",
|
|
1420
|
-
entry: "**/.deepagents/agents/"
|
|
1421
|
-
},
|
|
1422
|
-
{
|
|
1423
|
-
target: "deepagents",
|
|
1424
1339
|
feature: "hooks",
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
{ target: "factorydroid", feature: "rules", entry: "**/.factory/rules/" },
|
|
1429
|
-
{
|
|
1430
|
-
target: "factorydroid",
|
|
1431
|
-
feature: "commands",
|
|
1432
|
-
entry: "**/.factory/commands/"
|
|
1340
|
+
processor: HooksProcessor,
|
|
1341
|
+
schema: HooksProcessorToolTargetSchema,
|
|
1342
|
+
factory: toolHooksFactories
|
|
1433
1343
|
},
|
|
1434
1344
|
{
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1345
|
+
feature: "permissions",
|
|
1346
|
+
processor: PermissionsProcessor,
|
|
1347
|
+
schema: PermissionsProcessorToolTargetSchema,
|
|
1348
|
+
factory: toolPermissionsFactories
|
|
1349
|
+
}
|
|
1350
|
+
];
|
|
1351
|
+
var getProcessorRegistryEntry = (feature) => {
|
|
1352
|
+
const entry = PROCESSOR_REGISTRY.find((e) => e.feature === feature);
|
|
1353
|
+
if (!entry) {
|
|
1354
|
+
throw new Error(`No processor registered for feature: ${feature}`);
|
|
1355
|
+
}
|
|
1356
|
+
return entry;
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
// src/cli/commands/gitignore-derive.ts
|
|
1360
|
+
var TARGETS_NOT_DERIVED = /* @__PURE__ */ new Set([
|
|
1361
|
+
"agentsskills",
|
|
1362
|
+
"antigravity",
|
|
1363
|
+
"augmentcode-legacy",
|
|
1364
|
+
"claudecode-legacy"
|
|
1365
|
+
]);
|
|
1366
|
+
var DERIVED_PATHS_NOT_GITIGNORED = /* @__PURE__ */ new Set([
|
|
1367
|
+
"**/.amp/settings.json",
|
|
1368
|
+
"**/.amp/settings.jsonc",
|
|
1369
|
+
"**/.antigravity/settings.json",
|
|
1370
|
+
"**/.claude/settings.json",
|
|
1371
|
+
"**/.claude/settings.local.json",
|
|
1372
|
+
"**/.factory/settings.json",
|
|
1373
|
+
"**/.gemini/settings.json",
|
|
1374
|
+
"**/.zed/settings.json",
|
|
1375
|
+
"**/.warp/settings.toml",
|
|
1376
|
+
"**/kilo.json",
|
|
1377
|
+
"**/kilo.jsonc",
|
|
1378
|
+
"**/opencode.json"
|
|
1379
|
+
]);
|
|
1380
|
+
var toPosix = (path2) => path2.replace(/\\/g, "/");
|
|
1381
|
+
var dirToGlob = (relativeDirPath) => `**/${toPosix(relativeDirPath).replace(/\/$/, "")}/`;
|
|
1382
|
+
var fileToGlob = (relativeDirPath, relativeFilePath) => {
|
|
1383
|
+
const hasDir = relativeDirPath && relativeDirPath !== ".";
|
|
1384
|
+
return `**/${toPosix(hasDir ? `${relativeDirPath}/${relativeFilePath}` : relativeFilePath)}`;
|
|
1385
|
+
};
|
|
1386
|
+
var supportsProject = (factory) => {
|
|
1387
|
+
if (typeof factory !== "object" || factory === null || !("meta" in factory)) return true;
|
|
1388
|
+
const meta = factory.meta;
|
|
1389
|
+
return meta?.supportsProject !== false;
|
|
1390
|
+
};
|
|
1391
|
+
var getProjectPaths = (factory) => factory.class.getSettablePaths({ global: false });
|
|
1392
|
+
var pushEntry = (entries, target, feature, entry) => {
|
|
1393
|
+
if (DERIVED_PATHS_NOT_GITIGNORED.has(entry)) return;
|
|
1394
|
+
entries.push({ target, feature, entry });
|
|
1395
|
+
};
|
|
1396
|
+
var deriveDirEntries = (factories, feature) => {
|
|
1397
|
+
const entries = [];
|
|
1398
|
+
for (const [target, factory] of factories) {
|
|
1399
|
+
if (TARGETS_NOT_DERIVED.has(target)) continue;
|
|
1400
|
+
if (!supportsProject(factory)) continue;
|
|
1401
|
+
const paths = getProjectPaths(factory);
|
|
1402
|
+
const dir = paths.relativeDirPath;
|
|
1403
|
+
if (!dir || dir === ".") continue;
|
|
1404
|
+
pushEntry(entries, target, feature, dirToGlob(dir));
|
|
1405
|
+
}
|
|
1406
|
+
return entries;
|
|
1407
|
+
};
|
|
1408
|
+
var deriveFileEntries = (factories, feature) => {
|
|
1409
|
+
const entries = [];
|
|
1410
|
+
for (const [target, factory] of factories) {
|
|
1411
|
+
if (TARGETS_NOT_DERIVED.has(target)) continue;
|
|
1412
|
+
if (!supportsProject(factory)) continue;
|
|
1413
|
+
const paths = getProjectPaths(factory);
|
|
1414
|
+
if (!paths.relativeFilePath) continue;
|
|
1415
|
+
pushEntry(entries, target, feature, fileToGlob(paths.relativeDirPath, paths.relativeFilePath));
|
|
1416
|
+
}
|
|
1417
|
+
return entries;
|
|
1418
|
+
};
|
|
1419
|
+
var deriveRulesEntries = () => {
|
|
1420
|
+
const entries = [];
|
|
1421
|
+
const factories = getProcessorRegistryEntry("rules").factory;
|
|
1422
|
+
for (const [target, factory] of factories) {
|
|
1423
|
+
if (TARGETS_NOT_DERIVED.has(target)) continue;
|
|
1424
|
+
const paths = getProjectPaths(factory);
|
|
1425
|
+
for (const root of [paths.root, ...paths.alternativeRoots ?? []]) {
|
|
1426
|
+
if (root)
|
|
1427
|
+
pushEntry(
|
|
1428
|
+
entries,
|
|
1429
|
+
target,
|
|
1430
|
+
"rules",
|
|
1431
|
+
fileToGlob(root.relativeDirPath, root.relativeFilePath)
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
const nonRootDir = paths.nonRoot?.relativeDirPath;
|
|
1435
|
+
if (nonRootDir && nonRootDir !== ".") {
|
|
1436
|
+
pushEntry(entries, target, "rules", dirToGlob(nonRootDir));
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return entries;
|
|
1440
|
+
};
|
|
1441
|
+
var DIR_FEATURES = /* @__PURE__ */ new Set(["commands", "skills", "subagents"]);
|
|
1442
|
+
var FILE_FEATURES = /* @__PURE__ */ new Set(["mcp", "hooks", "permissions", "ignore"]);
|
|
1443
|
+
var deriveFeatureGitignoreEntries = (feature) => {
|
|
1444
|
+
if (feature === "rules") return deriveRulesEntries();
|
|
1445
|
+
const factory = getProcessorRegistryEntry(feature).factory;
|
|
1446
|
+
if (DIR_FEATURES.has(feature)) return deriveDirEntries(factory, feature);
|
|
1447
|
+
if (FILE_FEATURES.has(feature)) return deriveFileEntries(factory, feature);
|
|
1448
|
+
return [];
|
|
1449
|
+
};
|
|
1450
|
+
var DERIVED_FEATURES = [
|
|
1451
|
+
"rules",
|
|
1452
|
+
"commands",
|
|
1453
|
+
"skills",
|
|
1454
|
+
"subagents",
|
|
1455
|
+
"mcp",
|
|
1456
|
+
"hooks",
|
|
1457
|
+
"permissions",
|
|
1458
|
+
"ignore"
|
|
1459
|
+
];
|
|
1460
|
+
var deriveAllGitignoreEntries = () => DERIVED_FEATURES.flatMap((feature) => deriveFeatureGitignoreEntries(feature));
|
|
1461
|
+
|
|
1462
|
+
// src/cli/commands/gitignore-entries.ts
|
|
1463
|
+
var normalizeGitignoreEntryTargets = (target) => {
|
|
1464
|
+
return typeof target === "string" ? [target] : target;
|
|
1465
|
+
};
|
|
1466
|
+
var HAND_MAINTAINED_GITIGNORE_ENTRIES = [
|
|
1467
|
+
// rulesync's own meta files (common scope).
|
|
1471
1468
|
{
|
|
1472
|
-
target:
|
|
1473
|
-
feature: "
|
|
1474
|
-
entry:
|
|
1469
|
+
target: "common",
|
|
1470
|
+
feature: "general",
|
|
1471
|
+
entry: `${RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH}/`
|
|
1475
1472
|
},
|
|
1473
|
+
{ target: "common", feature: "general", entry: ".rulesync/rules/*.local.md" },
|
|
1474
|
+
{ target: "common", feature: "general", entry: "rulesync.local.jsonc" },
|
|
1475
|
+
// AGENTS.local.md is placed in common scope (not rovodev-only) so that
|
|
1476
|
+
// local rule files are always gitignored regardless of which targets are enabled.
|
|
1477
|
+
{ target: "common", feature: "general", entry: "**/AGENTS.local.md" },
|
|
1478
|
+
// Local-root rule files: materialized outside getSettablePaths.
|
|
1479
|
+
{ target: "claudecode", feature: "rules", entry: `**/${CLAUDECODE_LOCAL_RULE_FILE_NAME}` },
|
|
1476
1480
|
{
|
|
1477
|
-
target:
|
|
1481
|
+
target: "claudecode",
|
|
1478
1482
|
feature: "rules",
|
|
1479
|
-
entry:
|
|
1480
|
-
},
|
|
1481
|
-
{ target: "copilot", feature: "commands", entry: "**/.github/prompts/" },
|
|
1482
|
-
{ target: "copilot", feature: "subagents", entry: "**/.github/agents/" },
|
|
1483
|
-
// Copilot CLI shares the project `.github/skills/` location with the Copilot
|
|
1484
|
-
// IDE target; its personal skills live under `~/.copilot/skills/` (home dir,
|
|
1485
|
-
// not gitignored at the project level).
|
|
1486
|
-
{ target: ["copilot", "copilotcli"], feature: "skills", entry: "**/.github/skills/" },
|
|
1487
|
-
{ target: "copilot", feature: "hooks", entry: "**/.github/hooks/" },
|
|
1488
|
-
{ target: "copilot", feature: "mcp", entry: "**/.vscode/mcp.json" },
|
|
1489
|
-
// GitHub Copilot CLI
|
|
1490
|
-
{
|
|
1491
|
-
target: "copilotcli",
|
|
1492
|
-
feature: "mcp",
|
|
1493
|
-
entry: "**/.copilot/mcp-config.json"
|
|
1494
|
-
},
|
|
1495
|
-
// Project: <project>/.github/agents/*.agent.md
|
|
1496
|
-
// Global: ~/.copilot/agents/
|
|
1497
|
-
{ target: "copilotcli", feature: "subagents", entry: "**/.github/agents/" },
|
|
1498
|
-
{ target: "copilotcli", feature: "subagents", entry: "**/.copilot/agents/" },
|
|
1499
|
-
// Project: <project>/.github/hooks/copilotcli-hooks.json
|
|
1500
|
-
// Global: ~/.copilot/hooks/copilot-hooks.json (rulesync convention pending
|
|
1501
|
-
// official documentation of a global hooks location)
|
|
1502
|
-
{ target: "copilotcli", feature: "hooks", entry: "**/.github/hooks/" },
|
|
1503
|
-
{ target: "copilotcli", feature: "hooks", entry: "**/.copilot/hooks/" },
|
|
1504
|
-
// Junie
|
|
1505
|
-
{ target: "junie", feature: "rules", entry: "**/.junie/guidelines.md" },
|
|
1506
|
-
{ target: "junie", feature: "mcp", entry: "**/.junie/mcp/mcp.json" },
|
|
1507
|
-
{ target: "junie", feature: "skills", entry: "**/.junie/skills/" },
|
|
1508
|
-
{ target: "junie", feature: "subagents", entry: "**/.junie/agents/" },
|
|
1509
|
-
// Kilo Code
|
|
1510
|
-
{ target: "kilo", feature: "rules", entry: "**/.kilo/rules/" },
|
|
1511
|
-
{ target: "kilo", feature: "skills", entry: "**/.kilo/skills/" },
|
|
1512
|
-
{ target: "kilo", feature: "commands", entry: "**/.kilo/workflows/" },
|
|
1513
|
-
{ target: "kilo", feature: "mcp", entry: "**/.kilo/mcp.json" },
|
|
1514
|
-
{ target: "kilo", feature: "ignore", entry: "**/.kilocodeignore" },
|
|
1515
|
-
// No `**/kilo.jsonc` entry: structurally identical to `opencode.jsonc` (no
|
|
1516
|
-
// entry). The Kilo translator preserves non-permissions Kilo settings on
|
|
1517
|
-
// round-trip, so the file is intended to be checked in by the user — adding
|
|
1518
|
-
// `**/kilo.jsonc` would be too aggressive.
|
|
1519
|
-
// Kiro
|
|
1520
|
-
{ target: "kiro", feature: "rules", entry: "**/.kiro/steering/" },
|
|
1521
|
-
{ target: "kiro", feature: "commands", entry: "**/.kiro/prompts/" },
|
|
1522
|
-
{ target: "kiro", feature: "skills", entry: "**/.kiro/skills/" },
|
|
1523
|
-
{ target: "kiro", feature: "subagents", entry: "**/.kiro/agents/" },
|
|
1524
|
-
{ target: "kiro", feature: "mcp", entry: "**/.kiro/settings/mcp.json" },
|
|
1525
|
-
{ target: "kiro", feature: "ignore", entry: "**/.aiignore" },
|
|
1526
|
-
// Kiro IDE and CLI write to the same `.kiro/` tree as the legacy `kiro` alias.
|
|
1527
|
-
// (Kiro IDE subagents are Markdown under `.kiro/agents/`, the CLI's are JSON —
|
|
1528
|
-
// both covered by the shared `**/.kiro/agents/` entry.)
|
|
1529
|
-
{ target: ["kiro-cli", "kiro-ide"], feature: "rules", entry: "**/.kiro/steering/" },
|
|
1530
|
-
{ target: ["kiro-cli", "kiro-ide"], feature: "commands", entry: "**/.kiro/prompts/" },
|
|
1531
|
-
{ target: ["kiro-cli", "kiro-ide"], feature: "skills", entry: "**/.kiro/skills/" },
|
|
1532
|
-
{ target: ["kiro-cli", "kiro-ide"], feature: "subagents", entry: "**/.kiro/agents/" },
|
|
1533
|
-
{ target: ["kiro-cli", "kiro-ide"], feature: "mcp", entry: "**/.kiro/settings/mcp.json" },
|
|
1534
|
-
{ target: ["kiro-cli", "kiro-ide"], feature: "ignore", entry: "**/.aiignore" },
|
|
1535
|
-
// Keep this after ignore entries like "**/.aiignore" so the exception remains effective.
|
|
1536
|
-
{ target: "common", feature: "general", entry: "!.rulesync/.aiignore" },
|
|
1537
|
-
// OpenCode
|
|
1538
|
-
{ target: "opencode", feature: "commands", entry: "**/.opencode/commands/" },
|
|
1539
|
-
{ target: "opencode", feature: "subagents", entry: "**/.opencode/agents/" },
|
|
1540
|
-
{ target: "opencode", feature: "skills", entry: "**/.opencode/skills/" },
|
|
1541
|
-
{ target: "opencode", feature: "mcp", entry: "**/.opencode/plugins/" },
|
|
1542
|
-
{ target: "opencode", feature: "general", entry: "**/.opencode/memories/" },
|
|
1543
|
-
{
|
|
1544
|
-
target: "opencode",
|
|
1545
|
-
feature: "general",
|
|
1546
|
-
entry: "**/.opencode/package-lock.json"
|
|
1483
|
+
entry: `**/${CLAUDECODE_DIR}/${CLAUDECODE_LOCAL_RULE_FILE_NAME}`
|
|
1547
1484
|
},
|
|
1548
|
-
//
|
|
1549
|
-
{ target: "
|
|
1550
|
-
{ target: "pi", feature: "commands", entry: "**/.pi/prompts/" },
|
|
1551
|
-
{ target: "pi", feature: "skills", entry: "**/.pi/skills/" },
|
|
1552
|
-
// Qwen Code
|
|
1553
|
-
{ target: "qwencode", feature: "rules", entry: "**/QWEN.md" },
|
|
1554
|
-
{ target: "qwencode", feature: "commands", entry: "**/.qwen/commands/" },
|
|
1555
|
-
{ target: "qwencode", feature: "subagents", entry: "**/.qwen/agents/" },
|
|
1556
|
-
{ target: "qwencode", feature: "skills", entry: "**/.qwen/skills/" },
|
|
1557
|
-
{ target: "qwencode", feature: "ignore", entry: "**/.qwenignore" },
|
|
1558
|
-
{ target: "qwencode", feature: "general", entry: "**/.qwen/memories/" },
|
|
1559
|
-
// mcp + hooks both write to `.qwen/settings.json`, shared with permissions.
|
|
1560
|
-
{ target: "qwencode", feature: "permissions", entry: "**/.qwen/settings.json" },
|
|
1561
|
-
// Replit
|
|
1562
|
-
{ target: "replit", feature: "rules", entry: "**/replit.md" },
|
|
1563
|
-
// Roo
|
|
1564
|
-
{ target: "roo", feature: "rules", entry: "**/.roo/rules/" },
|
|
1565
|
-
{ target: "roo", feature: "skills", entry: "**/.roo/skills/" },
|
|
1566
|
-
{ target: "roo", feature: "ignore", entry: "**/.rooignore" },
|
|
1567
|
-
{ target: "roo", feature: "mcp", entry: "**/.roo/mcp.json" },
|
|
1568
|
-
{ target: "roo", feature: "subagents", entry: "**/.roomodes" },
|
|
1569
|
-
// Rovodev
|
|
1485
|
+
// Third-party tool by-products rulesync gitignores but never writes itself.
|
|
1486
|
+
{ target: "claudecode", feature: "general", entry: `**/${CLAUDECODE_DIR}/*.lock` },
|
|
1570
1487
|
{
|
|
1571
|
-
target: "
|
|
1488
|
+
target: "claudecode",
|
|
1572
1489
|
feature: "general",
|
|
1573
|
-
entry:
|
|
1490
|
+
entry: `**/${CLAUDECODE_DIR}/${CLAUDECODE_SETTINGS_LOCAL_FILE_NAME}`
|
|
1574
1491
|
},
|
|
1575
|
-
{ target: "rovodev", feature: "subagents", entry: "**/.rovodev/subagents/" },
|
|
1576
|
-
{ target: "rovodev", feature: "skills", entry: "**/.rovodev/skills/" },
|
|
1577
1492
|
{
|
|
1578
|
-
target: "
|
|
1493
|
+
target: "claudecode",
|
|
1579
1494
|
feature: "general",
|
|
1580
|
-
entry:
|
|
1495
|
+
entry: `**/${CLAUDECODE_DIR}/${CLAUDECODE_MEMORIES_DIR_NAME}/`
|
|
1581
1496
|
},
|
|
1582
|
-
{ target: "
|
|
1583
|
-
|
|
1584
|
-
// Each rulesync feature maps one-to-one onto a TAKT facet directory.
|
|
1585
|
-
{ target: "takt", feature: "rules", entry: "**/.takt/facets/policies/" },
|
|
1586
|
-
{ target: "takt", feature: "skills", entry: "**/.takt/facets/knowledge/" },
|
|
1587
|
-
{ target: "takt", feature: "subagents", entry: "**/.takt/facets/personas/" },
|
|
1588
|
-
{ target: "takt", feature: "commands", entry: "**/.takt/facets/instructions/" },
|
|
1497
|
+
{ target: "opencode", feature: "general", entry: "**/.opencode/package-lock.json" },
|
|
1498
|
+
{ target: "rovodev", feature: "general", entry: "**/.rovodev/.rulesync/" },
|
|
1589
1499
|
{ target: "takt", feature: "general", entry: "**/.takt/runs/" },
|
|
1590
1500
|
{ target: "takt", feature: "general", entry: "**/.takt/tasks/" },
|
|
1591
1501
|
{ target: "takt", feature: "general", entry: "**/.takt/.cache/" },
|
|
1592
1502
|
{ target: "takt", feature: "general", entry: "**/.takt/config.yaml" },
|
|
1593
|
-
//
|
|
1594
|
-
//
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
{ target: "
|
|
1598
|
-
{ target: "devin", feature: "ignore", entry: "**/.devinignore" },
|
|
1599
|
-
{ target: "devin", feature: "mcp", entry: "**/.windsurf/mcp_config.json" },
|
|
1600
|
-
{ target: "devin", feature: "hooks", entry: "**/.windsurf/hooks.json" },
|
|
1601
|
-
{ target: "devin", feature: "skills", entry: "**/.devin/skills/" },
|
|
1503
|
+
// Augment Code's legacy single-file rules path: accepted on import but never
|
|
1504
|
+
// generated (so not in getSettablePaths), gitignored as a convenience.
|
|
1505
|
+
{ target: "augmentcode", feature: "rules", entry: "**/.augment-guidelines" },
|
|
1506
|
+
// Shared trees and global-scope outputs not produced via project getSettablePaths.
|
|
1507
|
+
{ target: "rovodev", feature: "skills", entry: "**/.agents/skills/" },
|
|
1602
1508
|
{ target: "devin", feature: "skills", entry: "**/.codeium/windsurf/skills/" },
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
{ target: "
|
|
1607
|
-
//
|
|
1608
|
-
{ target: "
|
|
1609
|
-
|
|
1610
|
-
{ target: "
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
//
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
// Warp reads project rules only from the root `AGENTS.md` (handled by the
|
|
1619
|
-
// shared AGENTS.md entry above); it does not read `.warp/memories/`, so no
|
|
1620
|
-
// rules entry under `.warp/` is emitted.
|
|
1621
|
-
{ target: "warp", feature: "mcp", entry: "**/.warp/.mcp.json" },
|
|
1622
|
-
{ target: "warp", feature: "skills", entry: "**/.warp/skills/" },
|
|
1623
|
-
// Zed
|
|
1624
|
-
// `.rules` is the project rules file. `.agents/skills/` is shared with the
|
|
1625
|
-
// antigravity targets (already registered above); re-tagging it for `zed`
|
|
1626
|
-
// ensures target-filtered gitignore runs still include it.
|
|
1627
|
-
// No entry for `.zed/settings.json` (MCP / ignore): it is a user-managed
|
|
1628
|
-
// file, like other tools' settings.json, so it is intentionally not ignored.
|
|
1629
|
-
{ target: "zed", feature: "rules", entry: "**/.rules" },
|
|
1630
|
-
{ target: "zed", feature: "skills", entry: "**/.agents/skills/" }
|
|
1509
|
+
{ target: "copilotcli", feature: "subagents", entry: "**/.copilot/agents/" },
|
|
1510
|
+
{ target: "copilotcli", feature: "mcp", entry: "**/.copilot/mcp-config.json" },
|
|
1511
|
+
{ target: "copilotcli", feature: "hooks", entry: "**/.copilot/hooks/" },
|
|
1512
|
+
{ target: "deepagents", feature: "hooks", entry: "**/.deepagents/hooks.json" },
|
|
1513
|
+
// Roo aggregates subagents into a single `.roomodes` file (no settable path).
|
|
1514
|
+
{ target: "roo", feature: "subagents", entry: "**/.roomodes" },
|
|
1515
|
+
// codexcli has no ignore processor; its `.codexignore` is a ghost entry.
|
|
1516
|
+
{ target: "codexcli", feature: "ignore", entry: "**/.codexignore" }
|
|
1517
|
+
];
|
|
1518
|
+
var GITIGNORE_ENTRY_REGISTRY = [
|
|
1519
|
+
...HAND_MAINTAINED_GITIGNORE_ENTRIES,
|
|
1520
|
+
// Every entry a tool actually emits, derived from its getSettablePaths.
|
|
1521
|
+
...deriveAllGitignoreEntries(),
|
|
1522
|
+
// Keep this after ignore entries like Junie's "**/.aiignore" so the exception remains effective.
|
|
1523
|
+
{ target: "common", feature: "general", entry: "!.rulesync/.aiignore" }
|
|
1631
1524
|
];
|
|
1632
1525
|
var ALL_GITIGNORE_ENTRIES = (() => {
|
|
1633
1526
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -2340,7 +2233,7 @@ function findApmLockDependency(lock, repoUrl) {
|
|
|
2340
2233
|
|
|
2341
2234
|
// src/lib/apm/apm-manifest.ts
|
|
2342
2235
|
import { join as join5 } from "path";
|
|
2343
|
-
import {
|
|
2236
|
+
import { load as load2 } from "js-yaml";
|
|
2344
2237
|
import { optional as optional2, z as z5 } from "zod/mini";
|
|
2345
2238
|
var APM_MANIFEST_FILE_NAME = "apm.yml";
|
|
2346
2239
|
var ApmObjectDependencySchema = z5.looseObject({
|
|
@@ -2548,34 +2441,7 @@ async function installApm(params) {
|
|
|
2548
2441
|
}
|
|
2549
2442
|
const existingLock = await readApmLock(projectRoot);
|
|
2550
2443
|
if (options.frozen) {
|
|
2551
|
-
|
|
2552
|
-
throw new Error(
|
|
2553
|
-
"Frozen install failed: rulesync-apm.lock.yaml is missing. Run 'rulesync install --mode apm' to create it."
|
|
2554
|
-
);
|
|
2555
|
-
}
|
|
2556
|
-
const missing = manifest.dependencies.filter(
|
|
2557
|
-
(dep) => !findApmLockDependency(existingLock, canonicalRepoUrl(dep))
|
|
2558
|
-
);
|
|
2559
|
-
if (missing.length > 0) {
|
|
2560
|
-
const names = missing.map((d) => d.gitUrl).join(", ");
|
|
2561
|
-
throw new Error(
|
|
2562
|
-
`Frozen install failed: rulesync-apm.lock.yaml is missing entries for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
|
|
2563
|
-
);
|
|
2564
|
-
}
|
|
2565
|
-
const drifted = manifest.dependencies.filter((dep) => {
|
|
2566
|
-
if (dep.ref === void 0) return false;
|
|
2567
|
-
const locked = findApmLockDependency(existingLock, canonicalRepoUrl(dep));
|
|
2568
|
-
return locked?.resolved_ref !== void 0 && locked.resolved_ref !== dep.ref;
|
|
2569
|
-
});
|
|
2570
|
-
if (drifted.length > 0) {
|
|
2571
|
-
const names = drifted.map((d) => {
|
|
2572
|
-
const locked = findApmLockDependency(existingLock, canonicalRepoUrl(d));
|
|
2573
|
-
return `${d.gitUrl} (manifest=${d.ref}, lock=${locked?.resolved_ref})`;
|
|
2574
|
-
}).join(", ");
|
|
2575
|
-
throw new Error(
|
|
2576
|
-
`Frozen install failed: manifest ref does not match rulesync-apm.lock.yaml for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
|
|
2577
|
-
);
|
|
2578
|
-
}
|
|
2444
|
+
assertFrozenLockCoversManifest({ existingLock, dependencies: manifest.dependencies });
|
|
2579
2445
|
}
|
|
2580
2446
|
const token = GitHubClient.resolveToken(options.token);
|
|
2581
2447
|
const client = new GitHubClient({ token });
|
|
@@ -2630,30 +2496,7 @@ async function installApm(params) {
|
|
|
2630
2496
|
}
|
|
2631
2497
|
}
|
|
2632
2498
|
if (existingLock) {
|
|
2633
|
-
|
|
2634
|
-
const toDelete = [];
|
|
2635
|
-
for (const prev of existingLock.dependencies) {
|
|
2636
|
-
for (const deployed of prev.deployed_files) {
|
|
2637
|
-
if (!newDeployedFiles.has(deployed)) {
|
|
2638
|
-
toDelete.push(deployed);
|
|
2639
|
-
}
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
for (const relativePath of toDelete) {
|
|
2643
|
-
if (posix2.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
|
|
2644
|
-
logger5.warn(`Refusing to remove stale apm file with suspicious path: "${relativePath}".`);
|
|
2645
|
-
continue;
|
|
2646
|
-
}
|
|
2647
|
-
try {
|
|
2648
|
-
checkPathTraversal({ relativePath, intendedRootDir: projectRoot });
|
|
2649
|
-
} catch {
|
|
2650
|
-
logger5.warn(`Refusing to remove stale apm file outside projectRoot: "${relativePath}".`);
|
|
2651
|
-
continue;
|
|
2652
|
-
}
|
|
2653
|
-
const absolute = join6(projectRoot, relativePath);
|
|
2654
|
-
await removeFile(absolute);
|
|
2655
|
-
logger5.debug(`Removed stale apm file: ${relativePath}`);
|
|
2656
|
-
}
|
|
2499
|
+
await removeStaleApmFiles({ existingLock, newLock, projectRoot, logger: logger5 });
|
|
2657
2500
|
}
|
|
2658
2501
|
if (!frozen) {
|
|
2659
2502
|
newLock.generated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2672,6 +2515,64 @@ async function installApm(params) {
|
|
|
2672
2515
|
failedDependencyCount: failedCount
|
|
2673
2516
|
};
|
|
2674
2517
|
}
|
|
2518
|
+
function assertFrozenLockCoversManifest(params) {
|
|
2519
|
+
const { existingLock, dependencies } = params;
|
|
2520
|
+
if (!existingLock) {
|
|
2521
|
+
throw new Error(
|
|
2522
|
+
"Frozen install failed: rulesync-apm.lock.yaml is missing. Run 'rulesync install --mode apm' to create it."
|
|
2523
|
+
);
|
|
2524
|
+
}
|
|
2525
|
+
const missing = dependencies.filter(
|
|
2526
|
+
(dep) => !findApmLockDependency(existingLock, canonicalRepoUrl(dep))
|
|
2527
|
+
);
|
|
2528
|
+
if (missing.length > 0) {
|
|
2529
|
+
const names = missing.map((d) => d.gitUrl).join(", ");
|
|
2530
|
+
throw new Error(
|
|
2531
|
+
`Frozen install failed: rulesync-apm.lock.yaml is missing entries for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
const drifted = dependencies.filter((dep) => {
|
|
2535
|
+
if (dep.ref === void 0) return false;
|
|
2536
|
+
const locked = findApmLockDependency(existingLock, canonicalRepoUrl(dep));
|
|
2537
|
+
return locked?.resolved_ref !== void 0 && locked.resolved_ref !== dep.ref;
|
|
2538
|
+
});
|
|
2539
|
+
if (drifted.length > 0) {
|
|
2540
|
+
const names = drifted.map((d) => {
|
|
2541
|
+
const locked = findApmLockDependency(existingLock, canonicalRepoUrl(d));
|
|
2542
|
+
return `${d.gitUrl} (manifest=${d.ref}, lock=${locked?.resolved_ref})`;
|
|
2543
|
+
}).join(", ");
|
|
2544
|
+
throw new Error(
|
|
2545
|
+
`Frozen install failed: manifest ref does not match rulesync-apm.lock.yaml for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
async function removeStaleApmFiles(params) {
|
|
2550
|
+
const { existingLock, newLock, projectRoot, logger: logger5 } = params;
|
|
2551
|
+
const newDeployedFiles = new Set(newLock.dependencies.flatMap((d) => d.deployed_files));
|
|
2552
|
+
const toDelete = [];
|
|
2553
|
+
for (const prev of existingLock.dependencies) {
|
|
2554
|
+
for (const deployed of prev.deployed_files) {
|
|
2555
|
+
if (!newDeployedFiles.has(deployed)) {
|
|
2556
|
+
toDelete.push(deployed);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
for (const relativePath of toDelete) {
|
|
2561
|
+
if (posix2.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
|
|
2562
|
+
logger5.warn(`Refusing to remove stale apm file with suspicious path: "${relativePath}".`);
|
|
2563
|
+
continue;
|
|
2564
|
+
}
|
|
2565
|
+
try {
|
|
2566
|
+
checkPathTraversal({ relativePath, intendedRootDir: projectRoot });
|
|
2567
|
+
} catch {
|
|
2568
|
+
logger5.warn(`Refusing to remove stale apm file outside projectRoot: "${relativePath}".`);
|
|
2569
|
+
continue;
|
|
2570
|
+
}
|
|
2571
|
+
const absolute = join6(projectRoot, relativePath);
|
|
2572
|
+
await removeFile(absolute);
|
|
2573
|
+
logger5.debug(`Removed stale apm file: ${relativePath}`);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2675
2576
|
async function installDependency(params) {
|
|
2676
2577
|
const { dep, client, semaphore, projectRoot, existingLock, frozen, update, logger: logger5 } = params;
|
|
2677
2578
|
const repoUrl = canonicalRepoUrl(dep);
|
|
@@ -2700,58 +2601,25 @@ async function installDependency(params) {
|
|
|
2700
2601
|
logger: logger5
|
|
2701
2602
|
});
|
|
2702
2603
|
if (files.length === 0) continue;
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
const deployRelative = toPosixPath(join6(primitive.deployDir, relativeToBase));
|
|
2718
|
-
checkPathTraversal({
|
|
2719
|
-
relativePath: deployRelative,
|
|
2720
|
-
intendedRootDir: projectRoot
|
|
2721
|
-
});
|
|
2722
|
-
const content = await withSemaphore(
|
|
2723
|
-
semaphore,
|
|
2724
|
-
() => client.getFileContent(dep.owner, dep.repo, file.path, resolvedSha)
|
|
2725
|
-
);
|
|
2726
|
-
const byteLength = Buffer.byteLength(content, "utf8");
|
|
2727
|
-
if (byteLength > MAX_FILE_SIZE) {
|
|
2728
|
-
logger5.warn(
|
|
2729
|
-
`Skipping "${file.path}" from ${repoUrl}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
2730
|
-
);
|
|
2731
|
-
continue;
|
|
2732
|
-
}
|
|
2733
|
-
deployed.push({ path: deployRelative, content });
|
|
2734
|
-
if (!frozen) {
|
|
2735
|
-
await writeFileContent(join6(projectRoot, deployRelative), content);
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2604
|
+
await collectPrimitiveDeployments({
|
|
2605
|
+
dep,
|
|
2606
|
+
client,
|
|
2607
|
+
semaphore,
|
|
2608
|
+
projectRoot,
|
|
2609
|
+
primitive,
|
|
2610
|
+
remoteBase,
|
|
2611
|
+
files,
|
|
2612
|
+
resolvedSha,
|
|
2613
|
+
repoUrl,
|
|
2614
|
+
frozen,
|
|
2615
|
+
deployed,
|
|
2616
|
+
logger: logger5
|
|
2617
|
+
});
|
|
2738
2618
|
}
|
|
2739
2619
|
deployed.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
2740
2620
|
const deployedFiles = deployed.map((d) => d.path);
|
|
2741
2621
|
const contentHash = computeContentHash(deployed);
|
|
2742
|
-
|
|
2743
|
-
if (RULESYNC_CONTENT_HASH_REGEX.test(locked.content_hash)) {
|
|
2744
|
-
if (locked.content_hash !== contentHash) {
|
|
2745
|
-
throw new Error(
|
|
2746
|
-
`content_hash mismatch for ${repoUrl}: lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
|
|
2747
|
-
);
|
|
2748
|
-
}
|
|
2749
|
-
} else {
|
|
2750
|
-
logger5.debug(
|
|
2751
|
-
`Skipping content_hash integrity check for ${repoUrl}: recorded hash "${locked.content_hash}" was not written by rulesync.`
|
|
2752
|
-
);
|
|
2753
|
-
}
|
|
2754
|
-
}
|
|
2622
|
+
assertFrozenContentHashMatches({ frozen, locked, contentHash, repoUrl, logger: logger5 });
|
|
2755
2623
|
if (frozen) {
|
|
2756
2624
|
for (const { path: deployRelative, content } of deployed) {
|
|
2757
2625
|
await writeFileContent(join6(projectRoot, deployRelative), content);
|
|
@@ -2772,20 +2640,85 @@ async function installDependency(params) {
|
|
|
2772
2640
|
logger5.info(`Installed ${deployedFiles.length} file(s) from ${repoUrl}@${shortSha(resolvedSha)}`);
|
|
2773
2641
|
return { lockEntry, deployedFiles };
|
|
2774
2642
|
}
|
|
2775
|
-
function
|
|
2776
|
-
const
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2643
|
+
async function collectPrimitiveDeployments(params) {
|
|
2644
|
+
const {
|
|
2645
|
+
dep,
|
|
2646
|
+
client,
|
|
2647
|
+
semaphore,
|
|
2648
|
+
projectRoot,
|
|
2649
|
+
primitive,
|
|
2650
|
+
remoteBase,
|
|
2651
|
+
files,
|
|
2652
|
+
resolvedSha,
|
|
2653
|
+
repoUrl,
|
|
2654
|
+
frozen,
|
|
2655
|
+
deployed,
|
|
2656
|
+
logger: logger5
|
|
2657
|
+
} = params;
|
|
2658
|
+
for (const file of files) {
|
|
2659
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
2660
|
+
logger5.warn(
|
|
2661
|
+
`Skipping "${file.path}" from ${repoUrl}: ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
2662
|
+
);
|
|
2663
|
+
continue;
|
|
2664
|
+
}
|
|
2665
|
+
const relativeToBase = posix2.relative(remoteBase, toPosixPath(file.path));
|
|
2666
|
+
if (!relativeToBase || relativeToBase.startsWith("..") || posix2.isAbsolute(relativeToBase)) {
|
|
2667
|
+
logger5.warn(`Skipping "${file.path}" from ${repoUrl}: resolved outside of "${remoteBase}".`);
|
|
2668
|
+
continue;
|
|
2669
|
+
}
|
|
2670
|
+
const deployRelative = toPosixPath(join6(primitive.deployDir, relativeToBase));
|
|
2671
|
+
checkPathTraversal({
|
|
2672
|
+
relativePath: deployRelative,
|
|
2673
|
+
intendedRootDir: projectRoot
|
|
2674
|
+
});
|
|
2675
|
+
const content = await withSemaphore(
|
|
2676
|
+
semaphore,
|
|
2677
|
+
() => client.getFileContent(dep.owner, dep.repo, file.path, resolvedSha)
|
|
2678
|
+
);
|
|
2679
|
+
const byteLength = Buffer.byteLength(content, "utf8");
|
|
2680
|
+
if (byteLength > MAX_FILE_SIZE) {
|
|
2681
|
+
logger5.warn(
|
|
2682
|
+
`Skipping "${file.path}" from ${repoUrl}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
2683
|
+
);
|
|
2684
|
+
continue;
|
|
2685
|
+
}
|
|
2686
|
+
deployed.push({ path: deployRelative, content });
|
|
2687
|
+
if (!frozen) {
|
|
2688
|
+
await writeFileContent(join6(projectRoot, deployRelative), content);
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
function assertFrozenContentHashMatches(params) {
|
|
2693
|
+
const { frozen, locked, contentHash, repoUrl, logger: logger5 } = params;
|
|
2694
|
+
if (frozen && locked?.content_hash) {
|
|
2695
|
+
if (RULESYNC_CONTENT_HASH_REGEX.test(locked.content_hash)) {
|
|
2696
|
+
if (locked.content_hash !== contentHash) {
|
|
2697
|
+
throw new Error(
|
|
2698
|
+
`content_hash mismatch for ${repoUrl}: lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
|
|
2699
|
+
);
|
|
2700
|
+
}
|
|
2701
|
+
} else {
|
|
2702
|
+
logger5.debug(
|
|
2703
|
+
`Skipping content_hash integrity check for ${repoUrl}: recorded hash "${locked.content_hash}" was not written by rulesync.`
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
function computeContentHash(files) {
|
|
2709
|
+
const hash = createHash("sha256");
|
|
2710
|
+
for (const { path: path2, content } of files) {
|
|
2711
|
+
hash.update(path2);
|
|
2712
|
+
hash.update("\0");
|
|
2713
|
+
hash.update(content);
|
|
2714
|
+
hash.update("\0");
|
|
2715
|
+
}
|
|
2716
|
+
return `sha256:${hash.digest("hex")}`;
|
|
2717
|
+
}
|
|
2718
|
+
async function listPrimitiveFiles(params) {
|
|
2719
|
+
const { client, semaphore, owner, repo, ref, remoteBase, logger: logger5 } = params;
|
|
2720
|
+
try {
|
|
2721
|
+
return await listDirectoryRecursive({
|
|
2789
2722
|
client,
|
|
2790
2723
|
owner,
|
|
2791
2724
|
repo,
|
|
@@ -2814,7 +2747,7 @@ import { basename, join as join9, posix as posix3 } from "path";
|
|
|
2814
2747
|
import { Semaphore as Semaphore3 } from "es-toolkit/promise";
|
|
2815
2748
|
|
|
2816
2749
|
// src/lib/gh/gh-frontmatter.ts
|
|
2817
|
-
import { dump as
|
|
2750
|
+
import { dump as dump2, load as load3 } from "js-yaml";
|
|
2818
2751
|
var FRONTMATTER_FENCE = "---";
|
|
2819
2752
|
function injectSourceMetadata(params) {
|
|
2820
2753
|
const { content, source, repository, ref } = params;
|
|
@@ -2829,7 +2762,7 @@ function injectSourceMetadata(params) {
|
|
|
2829
2762
|
} else if (content === FRONTMATTER_FENCE) {
|
|
2830
2763
|
openFenceLen = 3;
|
|
2831
2764
|
} else {
|
|
2832
|
-
const yaml2 =
|
|
2765
|
+
const yaml2 = dump2(provenance, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2833
2766
|
return `${FRONTMATTER_FENCE}
|
|
2834
2767
|
${yaml2}${FRONTMATTER_FENCE}
|
|
2835
2768
|
${content}`;
|
|
@@ -2856,7 +2789,7 @@ ${content}`;
|
|
|
2856
2789
|
throw new Error("invalid frontmatter");
|
|
2857
2790
|
}
|
|
2858
2791
|
if (loaded === null || loaded === void 0) {
|
|
2859
|
-
const yaml2 =
|
|
2792
|
+
const yaml2 = dump2(provenance, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2860
2793
|
return `${FRONTMATTER_FENCE}
|
|
2861
2794
|
${yaml2}${FRONTMATTER_FENCE}
|
|
2862
2795
|
${rest}`;
|
|
@@ -2869,7 +2802,7 @@ ${rest}`;
|
|
|
2869
2802
|
...existing,
|
|
2870
2803
|
...provenance
|
|
2871
2804
|
};
|
|
2872
|
-
const yaml =
|
|
2805
|
+
const yaml = dump2(merged, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2873
2806
|
return `${FRONTMATTER_FENCE}
|
|
2874
2807
|
${yaml}${FRONTMATTER_FENCE}
|
|
2875
2808
|
${rest}`;
|
|
@@ -2877,7 +2810,7 @@ ${rest}`;
|
|
|
2877
2810
|
|
|
2878
2811
|
// src/lib/gh/gh-lock.ts
|
|
2879
2812
|
import { join as join7 } from "path";
|
|
2880
|
-
import { dump as
|
|
2813
|
+
import { dump as dump3, load as load4 } from "js-yaml";
|
|
2881
2814
|
import { optional as optional3, refine as refine2, z as z6 } from "zod/mini";
|
|
2882
2815
|
var GH_LOCKFILE_FILE_NAME = "rulesync-gh.lock.yaml";
|
|
2883
2816
|
var GH_LOCKFILE_VERSION = "1";
|
|
@@ -2949,7 +2882,7 @@ async function writeGhLock(params) {
|
|
|
2949
2882
|
await writeFileContent(path2, content);
|
|
2950
2883
|
}
|
|
2951
2884
|
function serializeGhLock(lock) {
|
|
2952
|
-
return
|
|
2885
|
+
return dump3(lock, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2953
2886
|
}
|
|
2954
2887
|
function findGhLockInstallation(lock, params) {
|
|
2955
2888
|
const target = params.source.toLowerCase();
|
|
@@ -3000,39 +2933,7 @@ async function installGh(params) {
|
|
|
3000
2933
|
if (sources.length === 0) {
|
|
3001
2934
|
return { sourcesProcessed: 0, installedSkillCount: 0, failedSourceCount: 0 };
|
|
3002
2935
|
}
|
|
3003
|
-
const resolvedSources = sources.map(
|
|
3004
|
-
const parsed = parseSource(entry.source);
|
|
3005
|
-
if (parsed.provider !== "github") {
|
|
3006
|
-
throw new Error(
|
|
3007
|
-
`--mode gh only supports GitHub sources. "${entry.source}" resolves to provider "${parsed.provider}".`
|
|
3008
|
-
);
|
|
3009
|
-
}
|
|
3010
|
-
if (entry.transport !== void 0 && entry.transport !== "github") {
|
|
3011
|
-
throw new Error(
|
|
3012
|
-
`--mode gh: field "transport" is not supported (got "${entry.transport}" for source "${entry.source}"). Drop the field or switch to --mode rulesync.`
|
|
3013
|
-
);
|
|
3014
|
-
}
|
|
3015
|
-
if (entry.path !== void 0) {
|
|
3016
|
-
throw new Error(
|
|
3017
|
-
`--mode gh: field "path" is not supported for source "${entry.source}". The remote layout is fixed to "skills/<name>/SKILL.md".`
|
|
3018
|
-
);
|
|
3019
|
-
}
|
|
3020
|
-
const agent = entry.agent ?? "github-copilot";
|
|
3021
|
-
if (!GH_AGENTS.includes(agent)) {
|
|
3022
|
-
throw new Error(
|
|
3023
|
-
`--mode gh: unknown agent "${agent}" for source "${entry.source}". Valid agents: ${GH_AGENTS.join(", ")}.`
|
|
3024
|
-
);
|
|
3025
|
-
}
|
|
3026
|
-
const scope = entry.scope ?? "project";
|
|
3027
|
-
return {
|
|
3028
|
-
entry,
|
|
3029
|
-
owner: parsed.owner,
|
|
3030
|
-
repo: parsed.repo,
|
|
3031
|
-
ref: entry.ref ?? parsed.ref,
|
|
3032
|
-
agent,
|
|
3033
|
-
scope
|
|
3034
|
-
};
|
|
3035
|
-
});
|
|
2936
|
+
const resolvedSources = sources.map(resolveGhSource);
|
|
3036
2937
|
const existingLock = await readGhLock(projectRoot);
|
|
3037
2938
|
const frozen = options.frozen ?? false;
|
|
3038
2939
|
const update = options.update ?? false;
|
|
@@ -3042,38 +2943,7 @@ async function installGh(params) {
|
|
|
3042
2943
|
);
|
|
3043
2944
|
}
|
|
3044
2945
|
if (frozen && existingLock) {
|
|
3045
|
-
|
|
3046
|
-
for (const rs of resolvedSources) {
|
|
3047
|
-
const hasAny = existingLock.installations.some(
|
|
3048
|
-
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase() && i.agent === rs.agent && i.scope === rs.scope
|
|
3049
|
-
);
|
|
3050
|
-
if (!hasAny) {
|
|
3051
|
-
uncovered.push(`${rs.entry.source} (agent=${rs.agent}, scope=${rs.scope})`);
|
|
3052
|
-
}
|
|
3053
|
-
}
|
|
3054
|
-
if (uncovered.length > 0) {
|
|
3055
|
-
throw new Error(
|
|
3056
|
-
`Frozen install failed: rulesync-gh.lock.yaml is missing entries for: ${uncovered.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
3057
|
-
);
|
|
3058
|
-
}
|
|
3059
|
-
const drifted = [];
|
|
3060
|
-
for (const rs of resolvedSources) {
|
|
3061
|
-
if (!rs.ref) continue;
|
|
3062
|
-
const matches = existingLock.installations.filter(
|
|
3063
|
-
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase()
|
|
3064
|
-
);
|
|
3065
|
-
for (const m of matches) {
|
|
3066
|
-
if (m.requested_ref !== void 0 && m.requested_ref !== rs.ref) {
|
|
3067
|
-
drifted.push(`${rs.entry.source} (manifest=${rs.ref}, lock=${m.requested_ref})`);
|
|
3068
|
-
break;
|
|
3069
|
-
}
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
if (drifted.length > 0) {
|
|
3073
|
-
throw new Error(
|
|
3074
|
-
`Frozen install failed: manifest ref does not match rulesync-gh.lock.yaml for: ${drifted.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
3075
|
-
);
|
|
3076
|
-
}
|
|
2946
|
+
assertFrozenLockCoversSources({ existingLock, resolvedSources });
|
|
3077
2947
|
}
|
|
3078
2948
|
const token = GitHubClient.resolveToken(options.token);
|
|
3079
2949
|
const client = new GitHubClient({ token });
|
|
@@ -3109,15 +2979,109 @@ async function installGh(params) {
|
|
|
3109
2979
|
})
|
|
3110
2980
|
);
|
|
3111
2981
|
if (frozen) {
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
2982
|
+
await writeDeferredFrozenFiles(results);
|
|
2983
|
+
}
|
|
2984
|
+
const { totalInstalled, failedCount } = aggregateSourceResults({ results, newLock });
|
|
2985
|
+
if (existingLock) {
|
|
2986
|
+
await removeStaleGhFiles({ existingLock, newLock, projectRoot, logger: logger5 });
|
|
2987
|
+
}
|
|
2988
|
+
if (!frozen) {
|
|
2989
|
+
newLock.generated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2990
|
+
await writeGhLock({ projectRoot, lock: newLock });
|
|
2991
|
+
if (failedCount === 0) {
|
|
2992
|
+
logger5.debug("rulesync-gh.lock.yaml updated.");
|
|
2993
|
+
} else {
|
|
2994
|
+
logger5.warn(
|
|
2995
|
+
`rulesync-gh.lock.yaml written with partially successful installs (${failedCount} source(s) failed).`
|
|
2996
|
+
);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
return {
|
|
3000
|
+
sourcesProcessed: sources.length,
|
|
3001
|
+
installedSkillCount: totalInstalled,
|
|
3002
|
+
failedSourceCount: failedCount
|
|
3003
|
+
};
|
|
3004
|
+
}
|
|
3005
|
+
function resolveGhSource(entry) {
|
|
3006
|
+
const parsed = parseSource(entry.source);
|
|
3007
|
+
if (parsed.provider !== "github") {
|
|
3008
|
+
throw new Error(
|
|
3009
|
+
`--mode gh only supports GitHub sources. "${entry.source}" resolves to provider "${parsed.provider}".`
|
|
3010
|
+
);
|
|
3011
|
+
}
|
|
3012
|
+
if (entry.transport !== void 0 && entry.transport !== "github") {
|
|
3013
|
+
throw new Error(
|
|
3014
|
+
`--mode gh: field "transport" is not supported (got "${entry.transport}" for source "${entry.source}"). Drop the field or switch to --mode rulesync.`
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
3017
|
+
if (entry.path !== void 0) {
|
|
3018
|
+
throw new Error(
|
|
3019
|
+
`--mode gh: field "path" is not supported for source "${entry.source}". The remote layout is fixed to "skills/<name>/SKILL.md".`
|
|
3020
|
+
);
|
|
3021
|
+
}
|
|
3022
|
+
const agent = entry.agent ?? "github-copilot";
|
|
3023
|
+
if (!GH_AGENTS.includes(agent)) {
|
|
3024
|
+
throw new Error(
|
|
3025
|
+
`--mode gh: unknown agent "${agent}" for source "${entry.source}". Valid agents: ${GH_AGENTS.join(", ")}.`
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
const scope = entry.scope ?? "project";
|
|
3029
|
+
return {
|
|
3030
|
+
entry,
|
|
3031
|
+
owner: parsed.owner,
|
|
3032
|
+
repo: parsed.repo,
|
|
3033
|
+
ref: entry.ref ?? parsed.ref,
|
|
3034
|
+
agent,
|
|
3035
|
+
scope
|
|
3036
|
+
};
|
|
3037
|
+
}
|
|
3038
|
+
function assertFrozenLockCoversSources(params) {
|
|
3039
|
+
const { existingLock, resolvedSources } = params;
|
|
3040
|
+
const uncovered = [];
|
|
3041
|
+
for (const rs of resolvedSources) {
|
|
3042
|
+
const hasAny = existingLock.installations.some(
|
|
3043
|
+
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase() && i.agent === rs.agent && i.scope === rs.scope
|
|
3044
|
+
);
|
|
3045
|
+
if (!hasAny) {
|
|
3046
|
+
uncovered.push(`${rs.entry.source} (agent=${rs.agent}, scope=${rs.scope})`);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
if (uncovered.length > 0) {
|
|
3050
|
+
throw new Error(
|
|
3051
|
+
`Frozen install failed: rulesync-gh.lock.yaml is missing entries for: ${uncovered.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
3052
|
+
);
|
|
3053
|
+
}
|
|
3054
|
+
const drifted = [];
|
|
3055
|
+
for (const rs of resolvedSources) {
|
|
3056
|
+
if (!rs.ref) continue;
|
|
3057
|
+
const matches = existingLock.installations.filter(
|
|
3058
|
+
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase()
|
|
3059
|
+
);
|
|
3060
|
+
for (const m of matches) {
|
|
3061
|
+
if (m.requested_ref !== void 0 && m.requested_ref !== rs.ref) {
|
|
3062
|
+
drifted.push(`${rs.entry.source} (manifest=${rs.ref}, lock=${m.requested_ref})`);
|
|
3063
|
+
break;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
if (drifted.length > 0) {
|
|
3068
|
+
throw new Error(
|
|
3069
|
+
`Frozen install failed: manifest ref does not match rulesync-gh.lock.yaml for: ${drifted.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
async function writeDeferredFrozenFiles(results) {
|
|
3074
|
+
for (const result of results) {
|
|
3075
|
+
if (result.status !== "ok") continue;
|
|
3076
|
+
for (const inst of result.installations) {
|
|
3077
|
+
for (const d of inst.deployed) {
|
|
3078
|
+
await writeFileContent(d.absolutePath, d.content);
|
|
3118
3079
|
}
|
|
3119
3080
|
}
|
|
3120
3081
|
}
|
|
3082
|
+
}
|
|
3083
|
+
function aggregateSourceResults(params) {
|
|
3084
|
+
const { results, newLock } = params;
|
|
3121
3085
|
let totalInstalled = 0;
|
|
3122
3086
|
let failedCount = 0;
|
|
3123
3087
|
for (const result of results) {
|
|
@@ -3133,47 +3097,135 @@ async function installGh(params) {
|
|
|
3133
3097
|
}
|
|
3134
3098
|
}
|
|
3135
3099
|
}
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
for (const deployed of prev.deployed_files) {
|
|
3145
|
-
const key = `${prev.scope}::${deployed}`;
|
|
3146
|
-
if (newDeployed.has(key)) continue;
|
|
3147
|
-
await removeStaleFile({
|
|
3148
|
-
relativePath: deployed,
|
|
3149
|
-
scope: prev.scope === "user" ? "user" : "project",
|
|
3150
|
-
projectRoot,
|
|
3151
|
-
logger: logger5
|
|
3152
|
-
});
|
|
3153
|
-
}
|
|
3100
|
+
return { totalInstalled, failedCount };
|
|
3101
|
+
}
|
|
3102
|
+
async function removeStaleGhFiles(params) {
|
|
3103
|
+
const { existingLock, newLock, projectRoot, logger: logger5 } = params;
|
|
3104
|
+
const newDeployed = /* @__PURE__ */ new Set();
|
|
3105
|
+
for (const inst of newLock.installations) {
|
|
3106
|
+
for (const file of inst.deployed_files) {
|
|
3107
|
+
newDeployed.add(`${inst.scope}::${file}`);
|
|
3154
3108
|
}
|
|
3155
3109
|
}
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3110
|
+
for (const prev of existingLock.installations) {
|
|
3111
|
+
for (const deployed of prev.deployed_files) {
|
|
3112
|
+
const key = `${prev.scope}::${deployed}`;
|
|
3113
|
+
if (newDeployed.has(key)) continue;
|
|
3114
|
+
await removeStaleFile({
|
|
3115
|
+
relativePath: deployed,
|
|
3116
|
+
scope: prev.scope === "user" ? "user" : "project",
|
|
3117
|
+
projectRoot,
|
|
3118
|
+
logger: logger5
|
|
3119
|
+
});
|
|
3165
3120
|
}
|
|
3166
3121
|
}
|
|
3167
|
-
return {
|
|
3168
|
-
sourcesProcessed: sources.length,
|
|
3169
|
-
installedSkillCount: totalInstalled,
|
|
3170
|
-
failedSourceCount: failedCount
|
|
3171
|
-
};
|
|
3172
3122
|
}
|
|
3173
3123
|
async function installSource(params) {
|
|
3174
3124
|
const { rs, client, semaphore, projectRoot, existingLock, frozen, update, logger: logger5 } = params;
|
|
3175
3125
|
const { entry, owner, repo, agent, scope } = rs;
|
|
3176
3126
|
const sourceKey = entry.source;
|
|
3127
|
+
const { resolvedRef, resolvedSha, usedTag } = await resolveGhRef({
|
|
3128
|
+
rs,
|
|
3129
|
+
client,
|
|
3130
|
+
owner,
|
|
3131
|
+
repo,
|
|
3132
|
+
sourceKey,
|
|
3133
|
+
logger: logger5
|
|
3134
|
+
});
|
|
3135
|
+
const validatedSkills = await discoverValidatedSkills({
|
|
3136
|
+
client,
|
|
3137
|
+
semaphore,
|
|
3138
|
+
owner,
|
|
3139
|
+
repo,
|
|
3140
|
+
resolvedSha,
|
|
3141
|
+
sourceKey,
|
|
3142
|
+
logger: logger5
|
|
3143
|
+
});
|
|
3144
|
+
if (validatedSkills === null) {
|
|
3145
|
+
return [];
|
|
3146
|
+
}
|
|
3147
|
+
const selected = selectSkills({ validatedSkills, entry, sourceKey, logger: logger5 });
|
|
3148
|
+
if (frozen && existingLock) {
|
|
3149
|
+
assertFrozenSkillCoverage({ selected, existingLock, sourceKey, agent, scope });
|
|
3150
|
+
}
|
|
3151
|
+
const results = [];
|
|
3152
|
+
const installRelDir = relativeInstallDirFor({ agent, scope });
|
|
3153
|
+
const scopeRoot = scope === "user" ? getHomeDirectory() : projectRoot;
|
|
3154
|
+
const sourceUrl = `https://github.com/${owner}/${repo}`;
|
|
3155
|
+
const repository = `${owner}/${repo}`;
|
|
3156
|
+
const provenanceRef = usedTag ? resolvedRef : resolvedSha;
|
|
3157
|
+
for (const sk of selected) {
|
|
3158
|
+
const locked = existingLock && !update ? findGhLockInstallation(existingLock, {
|
|
3159
|
+
source: sourceKey,
|
|
3160
|
+
agent,
|
|
3161
|
+
scope,
|
|
3162
|
+
skill: sk.name
|
|
3163
|
+
}) : void 0;
|
|
3164
|
+
const allFiles = await listDirectoryRecursive({
|
|
3165
|
+
client,
|
|
3166
|
+
owner,
|
|
3167
|
+
repo,
|
|
3168
|
+
path: sk.path,
|
|
3169
|
+
ref: resolvedSha,
|
|
3170
|
+
semaphore
|
|
3171
|
+
});
|
|
3172
|
+
const deployed = await buildSkillDeployment({
|
|
3173
|
+
sk,
|
|
3174
|
+
allFiles,
|
|
3175
|
+
client,
|
|
3176
|
+
semaphore,
|
|
3177
|
+
owner,
|
|
3178
|
+
repo,
|
|
3179
|
+
resolvedSha,
|
|
3180
|
+
installRelDir,
|
|
3181
|
+
scopeRoot,
|
|
3182
|
+
sourceUrl,
|
|
3183
|
+
repository,
|
|
3184
|
+
provenanceRef,
|
|
3185
|
+
sourceKey,
|
|
3186
|
+
frozen,
|
|
3187
|
+
logger: logger5
|
|
3188
|
+
});
|
|
3189
|
+
deployed.sort(
|
|
3190
|
+
(a, b) => a.relativeToScopeRoot < b.relativeToScopeRoot ? -1 : a.relativeToScopeRoot > b.relativeToScopeRoot ? 1 : 0
|
|
3191
|
+
);
|
|
3192
|
+
const deployedFiles = deployed.map((d) => d.relativeToScopeRoot);
|
|
3193
|
+
const contentHash = computeContentHash2(deployed);
|
|
3194
|
+
assertFrozenSkillIntegrity({
|
|
3195
|
+
frozen,
|
|
3196
|
+
locked,
|
|
3197
|
+
contentHash,
|
|
3198
|
+
sourceKey,
|
|
3199
|
+
skillName: sk.name,
|
|
3200
|
+
agent,
|
|
3201
|
+
scope,
|
|
3202
|
+
logger: logger5
|
|
3203
|
+
});
|
|
3204
|
+
const installation = {
|
|
3205
|
+
source: sourceKey,
|
|
3206
|
+
owner,
|
|
3207
|
+
repo,
|
|
3208
|
+
agent,
|
|
3209
|
+
scope,
|
|
3210
|
+
skill: sk.name,
|
|
3211
|
+
resolved_ref: resolvedRef,
|
|
3212
|
+
resolved_commit: resolvedSha,
|
|
3213
|
+
install_dir: toPosixPath(installRelDir),
|
|
3214
|
+
deployed_files: deployedFiles,
|
|
3215
|
+
content_hash: contentHash
|
|
3216
|
+
};
|
|
3217
|
+
if (rs.ref !== void 0) {
|
|
3218
|
+
installation.requested_ref = rs.ref;
|
|
3219
|
+
}
|
|
3220
|
+
results.push({ installation, deployed });
|
|
3221
|
+
logger5.info(
|
|
3222
|
+
`Installed gh skill "${sk.name}" from ${sourceKey} (agent=${agent}, scope=${scope}, ref=${resolvedRef})`
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
return results;
|
|
3226
|
+
}
|
|
3227
|
+
async function resolveGhRef(params) {
|
|
3228
|
+
const { rs, client, owner, repo, sourceKey, logger: logger5 } = params;
|
|
3177
3229
|
let resolvedRef;
|
|
3178
3230
|
let usedTag = false;
|
|
3179
3231
|
if (rs.ref) {
|
|
@@ -3193,13 +3245,17 @@ async function installSource(params) {
|
|
|
3193
3245
|
}
|
|
3194
3246
|
const resolvedSha = await client.resolveRefToSha(owner, repo, resolvedRef);
|
|
3195
3247
|
logger5.debug(`Resolved ${sourceKey} -> ref=${resolvedRef} sha=${resolvedSha}`);
|
|
3248
|
+
return { resolvedRef, resolvedSha, usedTag };
|
|
3249
|
+
}
|
|
3250
|
+
async function discoverValidatedSkills(params) {
|
|
3251
|
+
const { client, semaphore, owner, repo, resolvedSha, sourceKey, logger: logger5 } = params;
|
|
3196
3252
|
let topLevel;
|
|
3197
3253
|
try {
|
|
3198
3254
|
topLevel = await client.listDirectory(owner, repo, SKILLS_REMOTE_DIR, resolvedSha);
|
|
3199
3255
|
} catch (error) {
|
|
3200
3256
|
if (is404(error)) {
|
|
3201
3257
|
logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
|
|
3202
|
-
return
|
|
3258
|
+
return null;
|
|
3203
3259
|
}
|
|
3204
3260
|
throw error;
|
|
3205
3261
|
}
|
|
@@ -3214,152 +3270,133 @@ async function installSource(params) {
|
|
|
3214
3270
|
validatedSkills.push(sk);
|
|
3215
3271
|
}
|
|
3216
3272
|
}
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
if (!presentNames.has(want)) {
|
|
3224
|
-
logger5.warn(`Requested skill "${want}" not found in ${sourceKey} under skills/. Skipping.`);
|
|
3225
|
-
}
|
|
3226
|
-
}
|
|
3273
|
+
return validatedSkills;
|
|
3274
|
+
}
|
|
3275
|
+
function selectSkills(params) {
|
|
3276
|
+
const { validatedSkills, entry, sourceKey, logger: logger5 } = params;
|
|
3277
|
+
if (!entry.skills || entry.skills.length === 0) {
|
|
3278
|
+
return validatedSkills;
|
|
3227
3279
|
}
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
scope,
|
|
3235
|
-
skill: sk.name
|
|
3236
|
-
});
|
|
3237
|
-
if (!locked) {
|
|
3238
|
-
missing.push(sk.name);
|
|
3239
|
-
}
|
|
3240
|
-
}
|
|
3241
|
-
if (missing.length > 0) {
|
|
3242
|
-
throw new Error(
|
|
3243
|
-
`Frozen install failed: rulesync-gh.lock.yaml is missing entries for ${sourceKey} (agent=${agent}, scope=${scope}) skills: ${missing.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
3244
|
-
);
|
|
3280
|
+
const requested = new Set(entry.skills);
|
|
3281
|
+
const selected = validatedSkills.filter((s) => requested.has(s.name));
|
|
3282
|
+
const presentNames = new Set(validatedSkills.map((s) => s.name));
|
|
3283
|
+
for (const want of entry.skills) {
|
|
3284
|
+
if (!presentNames.has(want)) {
|
|
3285
|
+
logger5.warn(`Requested skill "${want}" not found in ${sourceKey} under skills/. Skipping.`);
|
|
3245
3286
|
}
|
|
3246
3287
|
}
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
const
|
|
3251
|
-
const
|
|
3252
|
-
const provenanceRef = usedTag ? resolvedRef : resolvedSha;
|
|
3288
|
+
return selected;
|
|
3289
|
+
}
|
|
3290
|
+
function assertFrozenSkillCoverage(params) {
|
|
3291
|
+
const { selected, existingLock, sourceKey, agent, scope } = params;
|
|
3292
|
+
const missing = [];
|
|
3253
3293
|
for (const sk of selected) {
|
|
3254
|
-
const locked =
|
|
3294
|
+
const locked = findGhLockInstallation(existingLock, {
|
|
3255
3295
|
source: sourceKey,
|
|
3256
3296
|
agent,
|
|
3257
3297
|
scope,
|
|
3258
3298
|
skill: sk.name
|
|
3259
|
-
}) : void 0;
|
|
3260
|
-
const allFiles = await listDirectoryRecursive({
|
|
3261
|
-
client,
|
|
3262
|
-
owner,
|
|
3263
|
-
repo,
|
|
3264
|
-
path: sk.path,
|
|
3265
|
-
ref: resolvedSha,
|
|
3266
|
-
semaphore
|
|
3267
3299
|
});
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3300
|
+
if (!locked) {
|
|
3301
|
+
missing.push(sk.name);
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
if (missing.length > 0) {
|
|
3305
|
+
throw new Error(
|
|
3306
|
+
`Frozen install failed: rulesync-gh.lock.yaml is missing entries for ${sourceKey} (agent=${agent}, scope=${scope}) skills: ${missing.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
3307
|
+
);
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
async function buildSkillDeployment(params) {
|
|
3311
|
+
const {
|
|
3312
|
+
sk,
|
|
3313
|
+
allFiles,
|
|
3314
|
+
client,
|
|
3315
|
+
semaphore,
|
|
3316
|
+
owner,
|
|
3317
|
+
repo,
|
|
3318
|
+
resolvedSha,
|
|
3319
|
+
installRelDir,
|
|
3320
|
+
scopeRoot,
|
|
3321
|
+
sourceUrl,
|
|
3322
|
+
repository,
|
|
3323
|
+
provenanceRef,
|
|
3324
|
+
sourceKey,
|
|
3325
|
+
frozen,
|
|
3326
|
+
logger: logger5
|
|
3327
|
+
} = params;
|
|
3328
|
+
const deployed = [];
|
|
3329
|
+
for (const file of allFiles) {
|
|
3330
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
3331
|
+
logger5.warn(
|
|
3332
|
+
`Skipping "${file.path}" from ${sourceKey}: ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
3333
|
+
);
|
|
3334
|
+
continue;
|
|
3335
|
+
}
|
|
3336
|
+
const relativeToSkill = posix3.relative(sk.path, toPosixPath(file.path));
|
|
3337
|
+
if (!relativeToSkill || relativeToSkill.startsWith("..") || posix3.isAbsolute(relativeToSkill)) {
|
|
3338
|
+
logger5.warn(`Skipping "${file.path}" from ${sourceKey}: resolved outside of "${sk.path}".`);
|
|
3339
|
+
continue;
|
|
3340
|
+
}
|
|
3341
|
+
const deployRelative = toPosixPath(join9(installRelDir, sk.name, relativeToSkill));
|
|
3342
|
+
checkPathTraversal({ relativePath: deployRelative, intendedRootDir: scopeRoot });
|
|
3343
|
+
const installAbs = join9(scopeRoot, installRelDir);
|
|
3344
|
+
const withinInstallDir = toPosixPath(join9(sk.name, relativeToSkill));
|
|
3345
|
+
checkPathTraversal({ relativePath: withinInstallDir, intendedRootDir: installAbs });
|
|
3346
|
+
let content = await withSemaphore(
|
|
3347
|
+
semaphore,
|
|
3348
|
+
() => client.getFileContent(owner, repo, file.path, resolvedSha)
|
|
3349
|
+
);
|
|
3350
|
+
const byteLength = Buffer.byteLength(content, "utf8");
|
|
3351
|
+
if (byteLength > MAX_FILE_SIZE) {
|
|
3352
|
+
logger5.warn(
|
|
3353
|
+
`Skipping "${file.path}" from ${sourceKey}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
3289
3354
|
);
|
|
3290
|
-
|
|
3291
|
-
|
|
3355
|
+
continue;
|
|
3356
|
+
}
|
|
3357
|
+
if (basename(file.path) === SKILL_FILE_NAME2) {
|
|
3358
|
+
try {
|
|
3359
|
+
content = injectSourceMetadata({
|
|
3360
|
+
content,
|
|
3361
|
+
source: sourceUrl,
|
|
3362
|
+
repository,
|
|
3363
|
+
ref: provenanceRef
|
|
3364
|
+
});
|
|
3365
|
+
} catch {
|
|
3292
3366
|
logger5.warn(
|
|
3293
|
-
`
|
|
3367
|
+
`Frontmatter in ${file.path} (${sourceKey}) is invalid. Prepending a fresh provenance block.`
|
|
3294
3368
|
);
|
|
3295
|
-
|
|
3296
|
-
}
|
|
3297
|
-
if (basename(file.path) === SKILL_FILE_NAME2) {
|
|
3298
|
-
try {
|
|
3299
|
-
content = injectSourceMetadata({
|
|
3300
|
-
content,
|
|
3301
|
-
source: sourceUrl,
|
|
3302
|
-
repository,
|
|
3303
|
-
ref: provenanceRef
|
|
3304
|
-
});
|
|
3305
|
-
} catch {
|
|
3306
|
-
logger5.warn(
|
|
3307
|
-
`Frontmatter in ${file.path} (${sourceKey}) is invalid. Prepending a fresh provenance block.`
|
|
3308
|
-
);
|
|
3309
|
-
content = `---
|
|
3369
|
+
content = `---
|
|
3310
3370
|
source: ${sourceUrl}
|
|
3311
3371
|
repository: ${repository}
|
|
3312
3372
|
ref: ${provenanceRef}
|
|
3313
3373
|
---
|
|
3314
3374
|
${content}`;
|
|
3315
|
-
}
|
|
3316
|
-
}
|
|
3317
|
-
const absolutePath = join9(scopeRoot, deployRelative);
|
|
3318
|
-
deployed.push({ relativeToScopeRoot: deployRelative, absolutePath, content });
|
|
3319
|
-
if (!frozen) {
|
|
3320
|
-
await writeFileContent(absolutePath, content);
|
|
3321
3375
|
}
|
|
3322
3376
|
}
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
)
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
`
|
|
3377
|
+
const absolutePath = join9(scopeRoot, deployRelative);
|
|
3378
|
+
deployed.push({ relativeToScopeRoot: deployRelative, absolutePath, content });
|
|
3379
|
+
if (!frozen) {
|
|
3380
|
+
await writeFileContent(absolutePath, content);
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
return deployed;
|
|
3384
|
+
}
|
|
3385
|
+
function assertFrozenSkillIntegrity(params) {
|
|
3386
|
+
const { frozen, locked, contentHash, sourceKey, skillName, agent, scope, logger: logger5 } = params;
|
|
3387
|
+
if (frozen && locked?.content_hash) {
|
|
3388
|
+
if (RULESYNC_CONTENT_HASH_REGEX2.test(locked.content_hash)) {
|
|
3389
|
+
if (locked.content_hash !== contentHash) {
|
|
3390
|
+
throw new Error(
|
|
3391
|
+
`content_hash mismatch for ${sourceKey} skill "${skillName}" (agent=${agent}, scope=${scope}): lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
|
|
3338
3392
|
);
|
|
3339
3393
|
}
|
|
3394
|
+
} else {
|
|
3395
|
+
logger5.debug(
|
|
3396
|
+
`Skipping content_hash integrity check for ${sourceKey} skill "${skillName}": recorded hash "${locked.content_hash}" was not written by rulesync.`
|
|
3397
|
+
);
|
|
3340
3398
|
}
|
|
3341
|
-
const installation = {
|
|
3342
|
-
source: sourceKey,
|
|
3343
|
-
owner,
|
|
3344
|
-
repo,
|
|
3345
|
-
agent,
|
|
3346
|
-
scope,
|
|
3347
|
-
skill: sk.name,
|
|
3348
|
-
resolved_ref: resolvedRef,
|
|
3349
|
-
resolved_commit: resolvedSha,
|
|
3350
|
-
install_dir: toPosixPath(installRelDir),
|
|
3351
|
-
deployed_files: deployedFiles,
|
|
3352
|
-
content_hash: contentHash
|
|
3353
|
-
};
|
|
3354
|
-
if (rs.ref !== void 0) {
|
|
3355
|
-
installation.requested_ref = rs.ref;
|
|
3356
|
-
}
|
|
3357
|
-
results.push({ installation, deployed });
|
|
3358
|
-
logger5.info(
|
|
3359
|
-
`Installed gh skill "${sk.name}" from ${sourceKey} (agent=${agent}, scope=${scope}, ref=${resolvedRef})`
|
|
3360
|
-
);
|
|
3361
3399
|
}
|
|
3362
|
-
return results;
|
|
3363
3400
|
}
|
|
3364
3401
|
async function removeStaleFile(params) {
|
|
3365
3402
|
const { relativePath, scope, projectRoot, logger: logger5 } = params;
|
|
@@ -3752,18 +3789,7 @@ async function resolveAndFetchSources(params) {
|
|
|
3752
3789
|
}
|
|
3753
3790
|
let lock = options.updateSources ? createEmptyLock() : await readLockFile({ projectRoot, logger: logger5 });
|
|
3754
3791
|
if (options.frozen) {
|
|
3755
|
-
|
|
3756
|
-
for (const source of sources) {
|
|
3757
|
-
const locked = getLockedSource(lock, source.source);
|
|
3758
|
-
if (!locked) {
|
|
3759
|
-
missingKeys.push(source.source);
|
|
3760
|
-
}
|
|
3761
|
-
}
|
|
3762
|
-
if (missingKeys.length > 0) {
|
|
3763
|
-
throw new Error(
|
|
3764
|
-
`Frozen install failed: lockfile is missing entries for: ${missingKeys.join(", ")}. Run 'rulesync install' to update the lockfile.`
|
|
3765
|
-
);
|
|
3766
|
-
}
|
|
3792
|
+
assertFrozenLockCoversSources2({ lock, sources });
|
|
3767
3793
|
}
|
|
3768
3794
|
const originalLockJson = JSON.stringify(lock);
|
|
3769
3795
|
const token = GitHubClient.resolveToken(options.token);
|
|
@@ -3773,31 +3799,17 @@ async function resolveAndFetchSources(params) {
|
|
|
3773
3799
|
const allFetchedSkillNames = /* @__PURE__ */ new Set();
|
|
3774
3800
|
for (const sourceEntry of sources) {
|
|
3775
3801
|
try {
|
|
3776
|
-
const
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
logger: logger5
|
|
3788
|
-
});
|
|
3789
|
-
} else {
|
|
3790
|
-
result = await fetchSource({
|
|
3791
|
-
sourceEntry,
|
|
3792
|
-
client,
|
|
3793
|
-
projectRoot,
|
|
3794
|
-
lock,
|
|
3795
|
-
localSkillNames,
|
|
3796
|
-
alreadyFetchedSkillNames: allFetchedSkillNames,
|
|
3797
|
-
updateSources: options.updateSources ?? false,
|
|
3798
|
-
logger: logger5
|
|
3799
|
-
});
|
|
3800
|
-
}
|
|
3802
|
+
const result = await fetchSourceByTransport({
|
|
3803
|
+
sourceEntry,
|
|
3804
|
+
client,
|
|
3805
|
+
projectRoot,
|
|
3806
|
+
lock,
|
|
3807
|
+
localSkillNames,
|
|
3808
|
+
alreadyFetchedSkillNames: allFetchedSkillNames,
|
|
3809
|
+
updateSources: options.updateSources ?? false,
|
|
3810
|
+
frozen: options.frozen ?? false,
|
|
3811
|
+
logger: logger5
|
|
3812
|
+
});
|
|
3801
3813
|
const { skillCount, fetchedSkillNames, updatedLock } = result;
|
|
3802
3814
|
lock = updatedLock;
|
|
3803
3815
|
totalSkillCount += skillCount;
|
|
@@ -3813,16 +3825,7 @@ async function resolveAndFetchSources(params) {
|
|
|
3813
3825
|
}
|
|
3814
3826
|
}
|
|
3815
3827
|
}
|
|
3816
|
-
|
|
3817
|
-
const prunedSources = {};
|
|
3818
|
-
for (const [key, value] of Object.entries(lock.sources)) {
|
|
3819
|
-
if (sourceKeys.has(normalizeSourceKey(key))) {
|
|
3820
|
-
prunedSources[key] = value;
|
|
3821
|
-
} else {
|
|
3822
|
-
logger5.debug(`Pruned stale lockfile entry: ${key}`);
|
|
3823
|
-
}
|
|
3824
|
-
}
|
|
3825
|
-
lock = { lockfileVersion: lock.lockfileVersion, sources: prunedSources };
|
|
3828
|
+
lock = pruneStaleLockEntries({ lock, sources, logger: logger5 });
|
|
3826
3829
|
if (!options.frozen && JSON.stringify(lock) !== originalLockJson) {
|
|
3827
3830
|
await writeLockFile({ projectRoot, lock, logger: logger5 });
|
|
3828
3831
|
} else {
|
|
@@ -3838,6 +3841,70 @@ function logGitClientHints(params) {
|
|
|
3838
3841
|
logger5.info("Hint: Check your git credentials (SSH keys, credential helper, or access token).");
|
|
3839
3842
|
}
|
|
3840
3843
|
}
|
|
3844
|
+
function assertFrozenLockCoversSources2(params) {
|
|
3845
|
+
const { lock, sources } = params;
|
|
3846
|
+
const missingKeys = [];
|
|
3847
|
+
for (const source of sources) {
|
|
3848
|
+
const locked = getLockedSource(lock, source.source);
|
|
3849
|
+
if (!locked) {
|
|
3850
|
+
missingKeys.push(source.source);
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
if (missingKeys.length > 0) {
|
|
3854
|
+
throw new Error(
|
|
3855
|
+
`Frozen install failed: lockfile is missing entries for: ${missingKeys.join(", ")}. Run 'rulesync install' to update the lockfile.`
|
|
3856
|
+
);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
async function fetchSourceByTransport(params) {
|
|
3860
|
+
const {
|
|
3861
|
+
sourceEntry,
|
|
3862
|
+
client,
|
|
3863
|
+
projectRoot,
|
|
3864
|
+
lock,
|
|
3865
|
+
localSkillNames,
|
|
3866
|
+
alreadyFetchedSkillNames,
|
|
3867
|
+
updateSources,
|
|
3868
|
+
frozen,
|
|
3869
|
+
logger: logger5
|
|
3870
|
+
} = params;
|
|
3871
|
+
const transport = sourceEntry.transport ?? "github";
|
|
3872
|
+
if (transport === "git") {
|
|
3873
|
+
return fetchSourceViaGit({
|
|
3874
|
+
sourceEntry,
|
|
3875
|
+
projectRoot,
|
|
3876
|
+
lock,
|
|
3877
|
+
localSkillNames,
|
|
3878
|
+
alreadyFetchedSkillNames,
|
|
3879
|
+
updateSources,
|
|
3880
|
+
frozen,
|
|
3881
|
+
logger: logger5
|
|
3882
|
+
});
|
|
3883
|
+
}
|
|
3884
|
+
return fetchSource({
|
|
3885
|
+
sourceEntry,
|
|
3886
|
+
client,
|
|
3887
|
+
projectRoot,
|
|
3888
|
+
lock,
|
|
3889
|
+
localSkillNames,
|
|
3890
|
+
alreadyFetchedSkillNames,
|
|
3891
|
+
updateSources,
|
|
3892
|
+
logger: logger5
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
function pruneStaleLockEntries(params) {
|
|
3896
|
+
const { lock, sources, logger: logger5 } = params;
|
|
3897
|
+
const sourceKeys = new Set(sources.map((s) => normalizeSourceKey(s.source)));
|
|
3898
|
+
const prunedSources = {};
|
|
3899
|
+
for (const [key, value] of Object.entries(lock.sources)) {
|
|
3900
|
+
if (sourceKeys.has(normalizeSourceKey(key))) {
|
|
3901
|
+
prunedSources[key] = value;
|
|
3902
|
+
} else {
|
|
3903
|
+
logger5.debug(`Pruned stale lockfile entry: ${key}`);
|
|
3904
|
+
}
|
|
3905
|
+
}
|
|
3906
|
+
return { lockfileVersion: lock.lockfileVersion, sources: prunedSources };
|
|
3907
|
+
}
|
|
3841
3908
|
async function checkLockedSkillsExist(curatedDir, skillNames) {
|
|
3842
3909
|
if (skillNames.length === 0) return true;
|
|
3843
3910
|
for (const name of skillNames) {
|
|
@@ -3969,7 +4036,189 @@ function groupRemoteFilesBySkillRoot(params) {
|
|
|
3969
4036
|
grouped.set(singleSkillName, rootLevelFiles);
|
|
3970
4037
|
}
|
|
3971
4038
|
}
|
|
3972
|
-
return grouped;
|
|
4039
|
+
return grouped;
|
|
4040
|
+
}
|
|
4041
|
+
async function resolveGithubFetchRef(params) {
|
|
4042
|
+
const { parsed, locked, updateSources, sourceKey, client, logger: logger5 } = params;
|
|
4043
|
+
if (locked && !updateSources) {
|
|
4044
|
+
logger5.debug(`Using locked ref for ${sourceKey}: ${locked.resolvedRef}`);
|
|
4045
|
+
return {
|
|
4046
|
+
ref: locked.resolvedRef,
|
|
4047
|
+
resolvedSha: locked.resolvedRef,
|
|
4048
|
+
requestedRef: locked.requestedRef
|
|
4049
|
+
};
|
|
4050
|
+
}
|
|
4051
|
+
const requestedRef = parsed.ref ?? await client.getDefaultBranch(parsed.owner, parsed.repo);
|
|
4052
|
+
const resolvedSha = await client.resolveRefToSha(parsed.owner, parsed.repo, requestedRef);
|
|
4053
|
+
logger5.debug(`Resolved ${sourceKey} ref "${requestedRef}" to SHA: ${resolvedSha}`);
|
|
4054
|
+
return { ref: resolvedSha, resolvedSha, requestedRef };
|
|
4055
|
+
}
|
|
4056
|
+
async function fetchRootLevelFallbackSkill(params) {
|
|
4057
|
+
const {
|
|
4058
|
+
entries,
|
|
4059
|
+
parsed,
|
|
4060
|
+
ref,
|
|
4061
|
+
resolvedSha,
|
|
4062
|
+
skillFilter,
|
|
4063
|
+
isWildcard,
|
|
4064
|
+
curatedDir,
|
|
4065
|
+
locked,
|
|
4066
|
+
sourceKey,
|
|
4067
|
+
localSkillNames,
|
|
4068
|
+
alreadyFetchedSkillNames,
|
|
4069
|
+
client,
|
|
4070
|
+
semaphore,
|
|
4071
|
+
fetchedSkills,
|
|
4072
|
+
logger: logger5
|
|
4073
|
+
} = params;
|
|
4074
|
+
const rootFiles = entries.filter((entry) => entry.type === "file");
|
|
4075
|
+
const rootSkillFiles = [];
|
|
4076
|
+
for (const file of rootFiles) {
|
|
4077
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
4078
|
+
logger5.warn(
|
|
4079
|
+
`Skipping file "${file.path}" (${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit).`
|
|
4080
|
+
);
|
|
4081
|
+
continue;
|
|
4082
|
+
}
|
|
4083
|
+
const content = await withSemaphore(
|
|
4084
|
+
semaphore,
|
|
4085
|
+
() => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
|
|
4086
|
+
);
|
|
4087
|
+
rootSkillFiles.push({ relativePath: file.name, content });
|
|
4088
|
+
}
|
|
4089
|
+
const groupedRootFiles = groupRemoteFilesBySkillRoot({
|
|
4090
|
+
remoteFiles: rootSkillFiles,
|
|
4091
|
+
skillFilter,
|
|
4092
|
+
isWildcard
|
|
4093
|
+
});
|
|
4094
|
+
const [fallbackSkillName] = groupedRootFiles.keys();
|
|
4095
|
+
if (fallbackSkillName === void 0) {
|
|
4096
|
+
return { handled: false, remoteSkillNames: [] };
|
|
4097
|
+
}
|
|
4098
|
+
if (!shouldSkipSkill({
|
|
4099
|
+
skillName: fallbackSkillName,
|
|
4100
|
+
sourceKey,
|
|
4101
|
+
localSkillNames,
|
|
4102
|
+
alreadyFetchedSkillNames,
|
|
4103
|
+
logger: logger5
|
|
4104
|
+
})) {
|
|
4105
|
+
fetchedSkills[fallbackSkillName] = await writeSkillAndComputeIntegrity({
|
|
4106
|
+
skillName: fallbackSkillName,
|
|
4107
|
+
files: groupedRootFiles.get(fallbackSkillName) ?? [],
|
|
4108
|
+
curatedDir,
|
|
4109
|
+
locked,
|
|
4110
|
+
resolvedSha,
|
|
4111
|
+
sourceKey,
|
|
4112
|
+
logger: logger5
|
|
4113
|
+
});
|
|
4114
|
+
logger5.debug(`Fetched skill "${fallbackSkillName}" from ${sourceKey}`);
|
|
4115
|
+
}
|
|
4116
|
+
return { handled: true, remoteSkillNames: [fallbackSkillName] };
|
|
4117
|
+
}
|
|
4118
|
+
async function fetchGithubSkillDir(params) {
|
|
4119
|
+
const {
|
|
4120
|
+
skillDir,
|
|
4121
|
+
parsed,
|
|
4122
|
+
ref,
|
|
4123
|
+
resolvedSha,
|
|
4124
|
+
curatedDir,
|
|
4125
|
+
locked,
|
|
4126
|
+
sourceKey,
|
|
4127
|
+
client,
|
|
4128
|
+
semaphore,
|
|
4129
|
+
logger: logger5
|
|
4130
|
+
} = params;
|
|
4131
|
+
const allFiles = await listDirectoryRecursive({
|
|
4132
|
+
client,
|
|
4133
|
+
owner: parsed.owner,
|
|
4134
|
+
repo: parsed.repo,
|
|
4135
|
+
path: skillDir.path,
|
|
4136
|
+
ref,
|
|
4137
|
+
semaphore
|
|
4138
|
+
});
|
|
4139
|
+
const files = allFiles.filter((file) => {
|
|
4140
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
4141
|
+
logger5.warn(
|
|
4142
|
+
`Skipping file "${file.path}" (${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit).`
|
|
4143
|
+
);
|
|
4144
|
+
return false;
|
|
4145
|
+
}
|
|
4146
|
+
return true;
|
|
4147
|
+
});
|
|
4148
|
+
const skillFiles = [];
|
|
4149
|
+
for (const file of files) {
|
|
4150
|
+
const relativeToSkill = file.path.substring(skillDir.path.length + 1);
|
|
4151
|
+
const content = await withSemaphore(
|
|
4152
|
+
semaphore,
|
|
4153
|
+
() => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
|
|
4154
|
+
);
|
|
4155
|
+
skillFiles.push({ relativePath: relativeToSkill, content });
|
|
4156
|
+
}
|
|
4157
|
+
return writeSkillAndComputeIntegrity({
|
|
4158
|
+
skillName: skillDir.name,
|
|
4159
|
+
files: skillFiles,
|
|
4160
|
+
curatedDir,
|
|
4161
|
+
locked,
|
|
4162
|
+
resolvedSha,
|
|
4163
|
+
sourceKey,
|
|
4164
|
+
logger: logger5
|
|
4165
|
+
});
|
|
4166
|
+
}
|
|
4167
|
+
async function discoverGithubSkillDirs(params) {
|
|
4168
|
+
const {
|
|
4169
|
+
parsed,
|
|
4170
|
+
ref,
|
|
4171
|
+
resolvedSha,
|
|
4172
|
+
skillFilter,
|
|
4173
|
+
isWildcard,
|
|
4174
|
+
curatedDir,
|
|
4175
|
+
locked,
|
|
4176
|
+
sourceKey,
|
|
4177
|
+
localSkillNames,
|
|
4178
|
+
alreadyFetchedSkillNames,
|
|
4179
|
+
client,
|
|
4180
|
+
semaphore,
|
|
4181
|
+
fetchedSkills,
|
|
4182
|
+
logger: logger5
|
|
4183
|
+
} = params;
|
|
4184
|
+
const skillsBasePath = parsed.path ?? "skills";
|
|
4185
|
+
try {
|
|
4186
|
+
const entries = await client.listDirectory(parsed.owner, parsed.repo, skillsBasePath, ref);
|
|
4187
|
+
const remoteSkillDirs = entries.filter((e) => e.type === "dir").map((e) => ({ name: e.name, path: e.path }));
|
|
4188
|
+
if (remoteSkillDirs.length === 0 && !isWildcard && skillFilter.length === 1) {
|
|
4189
|
+
const fallback = await fetchRootLevelFallbackSkill({
|
|
4190
|
+
entries,
|
|
4191
|
+
parsed,
|
|
4192
|
+
ref,
|
|
4193
|
+
resolvedSha,
|
|
4194
|
+
skillFilter,
|
|
4195
|
+
isWildcard,
|
|
4196
|
+
curatedDir,
|
|
4197
|
+
locked,
|
|
4198
|
+
sourceKey,
|
|
4199
|
+
localSkillNames,
|
|
4200
|
+
alreadyFetchedSkillNames,
|
|
4201
|
+
client,
|
|
4202
|
+
semaphore,
|
|
4203
|
+
fetchedSkills,
|
|
4204
|
+
logger: logger5
|
|
4205
|
+
});
|
|
4206
|
+
if (fallback.handled) {
|
|
4207
|
+
return {
|
|
4208
|
+
status: "ok",
|
|
4209
|
+
remoteSkillDirs,
|
|
4210
|
+
fallbackHandled: true,
|
|
4211
|
+
remoteSkillNames: fallback.remoteSkillNames
|
|
4212
|
+
};
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
return { status: "ok", remoteSkillDirs, fallbackHandled: false, remoteSkillNames: [] };
|
|
4216
|
+
} catch (error) {
|
|
4217
|
+
if (error instanceof GitHubClientError && error.statusCode === 404) {
|
|
4218
|
+
return { status: "notFound" };
|
|
4219
|
+
}
|
|
4220
|
+
throw error;
|
|
4221
|
+
}
|
|
3973
4222
|
}
|
|
3974
4223
|
async function fetchSource(params) {
|
|
3975
4224
|
const {
|
|
@@ -3990,20 +4239,14 @@ async function fetchSource(params) {
|
|
|
3990
4239
|
const sourceKey = sourceEntry.source;
|
|
3991
4240
|
const locked = getLockedSource(lock, sourceKey);
|
|
3992
4241
|
const lockedSkillNames = locked ? getLockedSkillNames(locked) : [];
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
} else {
|
|
4002
|
-
requestedRef = parsed.ref ?? await client.getDefaultBranch(parsed.owner, parsed.repo);
|
|
4003
|
-
resolvedSha = await client.resolveRefToSha(parsed.owner, parsed.repo, requestedRef);
|
|
4004
|
-
ref = resolvedSha;
|
|
4005
|
-
logger5.debug(`Resolved ${sourceKey} ref "${requestedRef}" to SHA: ${resolvedSha}`);
|
|
4006
|
-
}
|
|
4242
|
+
const { ref, resolvedSha, requestedRef } = await resolveGithubFetchRef({
|
|
4243
|
+
parsed,
|
|
4244
|
+
locked,
|
|
4245
|
+
updateSources,
|
|
4246
|
+
sourceKey,
|
|
4247
|
+
client,
|
|
4248
|
+
logger: logger5
|
|
4249
|
+
});
|
|
4007
4250
|
const curatedDir = join12(projectRoot, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
|
|
4008
4251
|
if (locked && resolvedSha === locked.resolvedRef && !updateSources) {
|
|
4009
4252
|
const allExist = await checkLockedSkillsExist(curatedDir, lockedSkillNames);
|
|
@@ -4020,69 +4263,29 @@ async function fetchSource(params) {
|
|
|
4020
4263
|
const isWildcard = skillFilter.length === 1 && skillFilter[0] === "*";
|
|
4021
4264
|
const semaphore = new Semaphore4(FETCH_CONCURRENCY_LIMIT);
|
|
4022
4265
|
const fetchedSkills = {};
|
|
4023
|
-
const
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
() => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
|
|
4043
|
-
);
|
|
4044
|
-
rootSkillFiles.push({ relativePath: file.name, content });
|
|
4045
|
-
}
|
|
4046
|
-
const groupedRootFiles = groupRemoteFilesBySkillRoot({
|
|
4047
|
-
remoteFiles: rootSkillFiles,
|
|
4048
|
-
skillFilter,
|
|
4049
|
-
isWildcard
|
|
4050
|
-
});
|
|
4051
|
-
const [fallbackSkillName] = groupedRootFiles.keys();
|
|
4052
|
-
if (fallbackSkillName !== void 0) {
|
|
4053
|
-
fallbackHandled = true;
|
|
4054
|
-
remoteSkillNames = [fallbackSkillName];
|
|
4055
|
-
if (!shouldSkipSkill({
|
|
4056
|
-
skillName: fallbackSkillName,
|
|
4057
|
-
sourceKey,
|
|
4058
|
-
localSkillNames,
|
|
4059
|
-
alreadyFetchedSkillNames,
|
|
4060
|
-
logger: logger5
|
|
4061
|
-
})) {
|
|
4062
|
-
fetchedSkills[fallbackSkillName] = await writeSkillAndComputeIntegrity({
|
|
4063
|
-
skillName: fallbackSkillName,
|
|
4064
|
-
files: groupedRootFiles.get(fallbackSkillName) ?? [],
|
|
4065
|
-
curatedDir,
|
|
4066
|
-
locked,
|
|
4067
|
-
resolvedSha,
|
|
4068
|
-
sourceKey,
|
|
4069
|
-
logger: logger5
|
|
4070
|
-
});
|
|
4071
|
-
logger5.debug(`Fetched skill "${fallbackSkillName}" from ${sourceKey}`);
|
|
4072
|
-
}
|
|
4073
|
-
}
|
|
4074
|
-
}
|
|
4075
|
-
} catch (error) {
|
|
4076
|
-
if (error instanceof GitHubClientError && error.statusCode === 404) {
|
|
4077
|
-
logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
|
|
4078
|
-
return { skillCount: 0, fetchedSkillNames: [], updatedLock: lock };
|
|
4079
|
-
}
|
|
4080
|
-
throw error;
|
|
4266
|
+
const discovery = await discoverGithubSkillDirs({
|
|
4267
|
+
parsed,
|
|
4268
|
+
ref,
|
|
4269
|
+
resolvedSha,
|
|
4270
|
+
skillFilter,
|
|
4271
|
+
isWildcard,
|
|
4272
|
+
curatedDir,
|
|
4273
|
+
locked,
|
|
4274
|
+
sourceKey,
|
|
4275
|
+
localSkillNames,
|
|
4276
|
+
alreadyFetchedSkillNames,
|
|
4277
|
+
client,
|
|
4278
|
+
semaphore,
|
|
4279
|
+
fetchedSkills,
|
|
4280
|
+
logger: logger5
|
|
4281
|
+
});
|
|
4282
|
+
if (discovery.status === "notFound") {
|
|
4283
|
+
logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
|
|
4284
|
+
return { skillCount: 0, fetchedSkillNames: [], updatedLock: lock };
|
|
4081
4285
|
}
|
|
4286
|
+
const { remoteSkillDirs, fallbackHandled, remoteSkillNames: fallbackSkillNames } = discovery;
|
|
4082
4287
|
const filteredDirs = isWildcard ? remoteSkillDirs : remoteSkillDirs.filter((d) => skillFilter.includes(d.name));
|
|
4083
|
-
|
|
4084
|
-
remoteSkillNames = filteredDirs.map((d) => d.name);
|
|
4085
|
-
}
|
|
4288
|
+
const remoteSkillNames = fallbackHandled ? fallbackSkillNames : filteredDirs.map((d) => d.name);
|
|
4086
4289
|
if (locked) {
|
|
4087
4290
|
await cleanPreviousCuratedSkills({ curatedDir, lockedSkillNames, logger: logger5 });
|
|
4088
4291
|
}
|
|
@@ -4096,39 +4299,16 @@ async function fetchSource(params) {
|
|
|
4096
4299
|
})) {
|
|
4097
4300
|
continue;
|
|
4098
4301
|
}
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
repo: parsed.repo,
|
|
4103
|
-
path: skillDir.path,
|
|
4302
|
+
fetchedSkills[skillDir.name] = await fetchGithubSkillDir({
|
|
4303
|
+
skillDir,
|
|
4304
|
+
parsed,
|
|
4104
4305
|
ref,
|
|
4105
|
-
|
|
4106
|
-
});
|
|
4107
|
-
const files = allFiles.filter((file) => {
|
|
4108
|
-
if (file.size > MAX_FILE_SIZE) {
|
|
4109
|
-
logger5.warn(
|
|
4110
|
-
`Skipping file "${file.path}" (${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit).`
|
|
4111
|
-
);
|
|
4112
|
-
return false;
|
|
4113
|
-
}
|
|
4114
|
-
return true;
|
|
4115
|
-
});
|
|
4116
|
-
const skillFiles = [];
|
|
4117
|
-
for (const file of files) {
|
|
4118
|
-
const relativeToSkill = file.path.substring(skillDir.path.length + 1);
|
|
4119
|
-
const content = await withSemaphore(
|
|
4120
|
-
semaphore,
|
|
4121
|
-
() => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
|
|
4122
|
-
);
|
|
4123
|
-
skillFiles.push({ relativePath: relativeToSkill, content });
|
|
4124
|
-
}
|
|
4125
|
-
fetchedSkills[skillDir.name] = await writeSkillAndComputeIntegrity({
|
|
4126
|
-
skillName: skillDir.name,
|
|
4127
|
-
files: skillFiles,
|
|
4306
|
+
resolvedSha,
|
|
4128
4307
|
curatedDir,
|
|
4129
4308
|
locked,
|
|
4130
|
-
resolvedSha,
|
|
4131
4309
|
sourceKey,
|
|
4310
|
+
client,
|
|
4311
|
+
semaphore,
|
|
4132
4312
|
logger: logger5
|
|
4133
4313
|
});
|
|
4134
4314
|
logger5.debug(`Fetched skill "${skillDir.name}" from ${sourceKey}`);
|
|
@@ -6037,6 +6217,174 @@ function ensureBody({ body, feature, operation }) {
|
|
|
6037
6217
|
}
|
|
6038
6218
|
return body;
|
|
6039
6219
|
}
|
|
6220
|
+
function requireContent({
|
|
6221
|
+
content,
|
|
6222
|
+
feature
|
|
6223
|
+
}) {
|
|
6224
|
+
if (!content) {
|
|
6225
|
+
throw new Error(`content is required for ${feature} put operation`);
|
|
6226
|
+
}
|
|
6227
|
+
return content;
|
|
6228
|
+
}
|
|
6229
|
+
function executeRule(parsed) {
|
|
6230
|
+
if (parsed.operation === "list") {
|
|
6231
|
+
return ruleTools.listRules.execute();
|
|
6232
|
+
}
|
|
6233
|
+
if (parsed.operation === "get") {
|
|
6234
|
+
return ruleTools.getRule.execute({ relativePathFromCwd: requireTargetPath(parsed) });
|
|
6235
|
+
}
|
|
6236
|
+
if (parsed.operation === "put") {
|
|
6237
|
+
return ruleTools.putRule.execute({
|
|
6238
|
+
relativePathFromCwd: requireTargetPath(parsed),
|
|
6239
|
+
frontmatter: parseFrontmatter({
|
|
6240
|
+
feature: "rule",
|
|
6241
|
+
frontmatter: parsed.frontmatter ?? {}
|
|
6242
|
+
}),
|
|
6243
|
+
body: ensureBody(parsed)
|
|
6244
|
+
});
|
|
6245
|
+
}
|
|
6246
|
+
return ruleTools.deleteRule.execute({ relativePathFromCwd: requireTargetPath(parsed) });
|
|
6247
|
+
}
|
|
6248
|
+
function executeCommand(parsed) {
|
|
6249
|
+
if (parsed.operation === "list") {
|
|
6250
|
+
return commandTools.listCommands.execute();
|
|
6251
|
+
}
|
|
6252
|
+
if (parsed.operation === "get") {
|
|
6253
|
+
return commandTools.getCommand.execute({
|
|
6254
|
+
relativePathFromCwd: requireTargetPath(parsed)
|
|
6255
|
+
});
|
|
6256
|
+
}
|
|
6257
|
+
if (parsed.operation === "put") {
|
|
6258
|
+
return commandTools.putCommand.execute({
|
|
6259
|
+
relativePathFromCwd: requireTargetPath(parsed),
|
|
6260
|
+
frontmatter: parseFrontmatter({
|
|
6261
|
+
feature: "command",
|
|
6262
|
+
frontmatter: parsed.frontmatter ?? {}
|
|
6263
|
+
}),
|
|
6264
|
+
body: ensureBody(parsed)
|
|
6265
|
+
});
|
|
6266
|
+
}
|
|
6267
|
+
return commandTools.deleteCommand.execute({
|
|
6268
|
+
relativePathFromCwd: requireTargetPath(parsed)
|
|
6269
|
+
});
|
|
6270
|
+
}
|
|
6271
|
+
function executeSubagent(parsed) {
|
|
6272
|
+
if (parsed.operation === "list") {
|
|
6273
|
+
return subagentTools.listSubagents.execute();
|
|
6274
|
+
}
|
|
6275
|
+
if (parsed.operation === "get") {
|
|
6276
|
+
return subagentTools.getSubagent.execute({
|
|
6277
|
+
relativePathFromCwd: requireTargetPath(parsed)
|
|
6278
|
+
});
|
|
6279
|
+
}
|
|
6280
|
+
if (parsed.operation === "put") {
|
|
6281
|
+
return subagentTools.putSubagent.execute({
|
|
6282
|
+
relativePathFromCwd: requireTargetPath(parsed),
|
|
6283
|
+
frontmatter: parseFrontmatter({
|
|
6284
|
+
feature: "subagent",
|
|
6285
|
+
frontmatter: parsed.frontmatter ?? {}
|
|
6286
|
+
}),
|
|
6287
|
+
body: ensureBody(parsed)
|
|
6288
|
+
});
|
|
6289
|
+
}
|
|
6290
|
+
return subagentTools.deleteSubagent.execute({
|
|
6291
|
+
relativePathFromCwd: requireTargetPath(parsed)
|
|
6292
|
+
});
|
|
6293
|
+
}
|
|
6294
|
+
function executeSkill(parsed) {
|
|
6295
|
+
if (parsed.operation === "list") {
|
|
6296
|
+
return skillTools.listSkills.execute();
|
|
6297
|
+
}
|
|
6298
|
+
if (parsed.operation === "get") {
|
|
6299
|
+
return skillTools.getSkill.execute({ relativeDirPathFromCwd: requireTargetPath(parsed) });
|
|
6300
|
+
}
|
|
6301
|
+
if (parsed.operation === "put") {
|
|
6302
|
+
return skillTools.putSkill.execute({
|
|
6303
|
+
relativeDirPathFromCwd: requireTargetPath(parsed),
|
|
6304
|
+
frontmatter: parseFrontmatter({
|
|
6305
|
+
feature: "skill",
|
|
6306
|
+
frontmatter: parsed.frontmatter ?? {}
|
|
6307
|
+
}),
|
|
6308
|
+
body: ensureBody(parsed),
|
|
6309
|
+
otherFiles: parsed.otherFiles ?? []
|
|
6310
|
+
});
|
|
6311
|
+
}
|
|
6312
|
+
return skillTools.deleteSkill.execute({
|
|
6313
|
+
relativeDirPathFromCwd: requireTargetPath(parsed)
|
|
6314
|
+
});
|
|
6315
|
+
}
|
|
6316
|
+
function executeIgnore(parsed) {
|
|
6317
|
+
if (parsed.operation === "get") {
|
|
6318
|
+
return ignoreTools.getIgnoreFile.execute();
|
|
6319
|
+
}
|
|
6320
|
+
if (parsed.operation === "put") {
|
|
6321
|
+
return ignoreTools.putIgnoreFile.execute({
|
|
6322
|
+
content: requireContent({ content: parsed.content, feature: "ignore" })
|
|
6323
|
+
});
|
|
6324
|
+
}
|
|
6325
|
+
return ignoreTools.deleteIgnoreFile.execute();
|
|
6326
|
+
}
|
|
6327
|
+
function executeMcp(parsed) {
|
|
6328
|
+
if (parsed.operation === "get") {
|
|
6329
|
+
return mcpTools.getMcpFile.execute();
|
|
6330
|
+
}
|
|
6331
|
+
if (parsed.operation === "put") {
|
|
6332
|
+
return mcpTools.putMcpFile.execute({
|
|
6333
|
+
content: requireContent({ content: parsed.content, feature: "mcp" })
|
|
6334
|
+
});
|
|
6335
|
+
}
|
|
6336
|
+
return mcpTools.deleteMcpFile.execute();
|
|
6337
|
+
}
|
|
6338
|
+
function executePermissions(parsed) {
|
|
6339
|
+
if (parsed.operation === "get") {
|
|
6340
|
+
return permissionsTools.getPermissionsFile.execute();
|
|
6341
|
+
}
|
|
6342
|
+
if (parsed.operation === "put") {
|
|
6343
|
+
return permissionsTools.putPermissionsFile.execute({
|
|
6344
|
+
content: requireContent({ content: parsed.content, feature: "permissions" })
|
|
6345
|
+
});
|
|
6346
|
+
}
|
|
6347
|
+
return permissionsTools.deletePermissionsFile.execute();
|
|
6348
|
+
}
|
|
6349
|
+
function executeHooks(parsed) {
|
|
6350
|
+
if (parsed.operation === "get") {
|
|
6351
|
+
return hooksTools.getHooksFile.execute();
|
|
6352
|
+
}
|
|
6353
|
+
if (parsed.operation === "put") {
|
|
6354
|
+
return hooksTools.putHooksFile.execute({
|
|
6355
|
+
content: requireContent({ content: parsed.content, feature: "hooks" })
|
|
6356
|
+
});
|
|
6357
|
+
}
|
|
6358
|
+
return hooksTools.deleteHooksFile.execute();
|
|
6359
|
+
}
|
|
6360
|
+
function executeGenerate2(parsed) {
|
|
6361
|
+
return generateTools.executeGenerate.execute(parsed.generateOptions ?? {});
|
|
6362
|
+
}
|
|
6363
|
+
function executeImport2(parsed) {
|
|
6364
|
+
if (!parsed.importOptions) {
|
|
6365
|
+
throw new Error("importOptions is required for import feature");
|
|
6366
|
+
}
|
|
6367
|
+
return importTools.executeImport.execute(parsed.importOptions);
|
|
6368
|
+
}
|
|
6369
|
+
function executeConvert2(parsed) {
|
|
6370
|
+
if (!parsed.convertOptions) {
|
|
6371
|
+
throw new Error("convertOptions is required for convert feature");
|
|
6372
|
+
}
|
|
6373
|
+
return convertTools.executeConvert.execute(parsed.convertOptions);
|
|
6374
|
+
}
|
|
6375
|
+
var featureExecutors = {
|
|
6376
|
+
rule: executeRule,
|
|
6377
|
+
command: executeCommand,
|
|
6378
|
+
subagent: executeSubagent,
|
|
6379
|
+
skill: executeSkill,
|
|
6380
|
+
ignore: executeIgnore,
|
|
6381
|
+
mcp: executeMcp,
|
|
6382
|
+
permissions: executePermissions,
|
|
6383
|
+
hooks: executeHooks,
|
|
6384
|
+
generate: executeGenerate2,
|
|
6385
|
+
import: executeImport2,
|
|
6386
|
+
convert: executeConvert2
|
|
6387
|
+
};
|
|
6040
6388
|
var rulesyncTool = {
|
|
6041
6389
|
name: "rulesyncTool",
|
|
6042
6390
|
description: "Manage Rulesync files through a single MCP tool. Features: rule/command/subagent/skill support list/get/put/delete; ignore/mcp/permissions/hooks support get/put/delete only; generate supports run only; import supports run only; convert supports run only. Parameters: list requires no targetPathFromCwd (lists all items); get/delete require targetPathFromCwd; put requires targetPathFromCwd, frontmatter, and body (or content for ignore/mcp/permissions/hooks); generate/run uses generateOptions to configure generation; import/run uses importOptions to configure import; convert/run uses convertOptions to configure conversion.",
|
|
@@ -6044,161 +6392,11 @@ var rulesyncTool = {
|
|
|
6044
6392
|
execute: async (args) => {
|
|
6045
6393
|
const parsed = rulesyncToolSchema.parse(args);
|
|
6046
6394
|
assertSupported({ feature: parsed.feature, operation: parsed.operation });
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
return ruleTools.listRules.execute();
|
|
6051
|
-
}
|
|
6052
|
-
if (parsed.operation === "get") {
|
|
6053
|
-
return ruleTools.getRule.execute({ relativePathFromCwd: requireTargetPath(parsed) });
|
|
6054
|
-
}
|
|
6055
|
-
if (parsed.operation === "put") {
|
|
6056
|
-
return ruleTools.putRule.execute({
|
|
6057
|
-
relativePathFromCwd: requireTargetPath(parsed),
|
|
6058
|
-
frontmatter: parseFrontmatter({
|
|
6059
|
-
feature: "rule",
|
|
6060
|
-
frontmatter: parsed.frontmatter ?? {}
|
|
6061
|
-
}),
|
|
6062
|
-
body: ensureBody(parsed)
|
|
6063
|
-
});
|
|
6064
|
-
}
|
|
6065
|
-
return ruleTools.deleteRule.execute({ relativePathFromCwd: requireTargetPath(parsed) });
|
|
6066
|
-
}
|
|
6067
|
-
case "command": {
|
|
6068
|
-
if (parsed.operation === "list") {
|
|
6069
|
-
return commandTools.listCommands.execute();
|
|
6070
|
-
}
|
|
6071
|
-
if (parsed.operation === "get") {
|
|
6072
|
-
return commandTools.getCommand.execute({
|
|
6073
|
-
relativePathFromCwd: requireTargetPath(parsed)
|
|
6074
|
-
});
|
|
6075
|
-
}
|
|
6076
|
-
if (parsed.operation === "put") {
|
|
6077
|
-
return commandTools.putCommand.execute({
|
|
6078
|
-
relativePathFromCwd: requireTargetPath(parsed),
|
|
6079
|
-
frontmatter: parseFrontmatter({
|
|
6080
|
-
feature: "command",
|
|
6081
|
-
frontmatter: parsed.frontmatter ?? {}
|
|
6082
|
-
}),
|
|
6083
|
-
body: ensureBody(parsed)
|
|
6084
|
-
});
|
|
6085
|
-
}
|
|
6086
|
-
return commandTools.deleteCommand.execute({
|
|
6087
|
-
relativePathFromCwd: requireTargetPath(parsed)
|
|
6088
|
-
});
|
|
6089
|
-
}
|
|
6090
|
-
case "subagent": {
|
|
6091
|
-
if (parsed.operation === "list") {
|
|
6092
|
-
return subagentTools.listSubagents.execute();
|
|
6093
|
-
}
|
|
6094
|
-
if (parsed.operation === "get") {
|
|
6095
|
-
return subagentTools.getSubagent.execute({
|
|
6096
|
-
relativePathFromCwd: requireTargetPath(parsed)
|
|
6097
|
-
});
|
|
6098
|
-
}
|
|
6099
|
-
if (parsed.operation === "put") {
|
|
6100
|
-
return subagentTools.putSubagent.execute({
|
|
6101
|
-
relativePathFromCwd: requireTargetPath(parsed),
|
|
6102
|
-
frontmatter: parseFrontmatter({
|
|
6103
|
-
feature: "subagent",
|
|
6104
|
-
frontmatter: parsed.frontmatter ?? {}
|
|
6105
|
-
}),
|
|
6106
|
-
body: ensureBody(parsed)
|
|
6107
|
-
});
|
|
6108
|
-
}
|
|
6109
|
-
return subagentTools.deleteSubagent.execute({
|
|
6110
|
-
relativePathFromCwd: requireTargetPath(parsed)
|
|
6111
|
-
});
|
|
6112
|
-
}
|
|
6113
|
-
case "skill": {
|
|
6114
|
-
if (parsed.operation === "list") {
|
|
6115
|
-
return skillTools.listSkills.execute();
|
|
6116
|
-
}
|
|
6117
|
-
if (parsed.operation === "get") {
|
|
6118
|
-
return skillTools.getSkill.execute({ relativeDirPathFromCwd: requireTargetPath(parsed) });
|
|
6119
|
-
}
|
|
6120
|
-
if (parsed.operation === "put") {
|
|
6121
|
-
return skillTools.putSkill.execute({
|
|
6122
|
-
relativeDirPathFromCwd: requireTargetPath(parsed),
|
|
6123
|
-
frontmatter: parseFrontmatter({
|
|
6124
|
-
feature: "skill",
|
|
6125
|
-
frontmatter: parsed.frontmatter ?? {}
|
|
6126
|
-
}),
|
|
6127
|
-
body: ensureBody(parsed),
|
|
6128
|
-
otherFiles: parsed.otherFiles ?? []
|
|
6129
|
-
});
|
|
6130
|
-
}
|
|
6131
|
-
return skillTools.deleteSkill.execute({
|
|
6132
|
-
relativeDirPathFromCwd: requireTargetPath(parsed)
|
|
6133
|
-
});
|
|
6134
|
-
}
|
|
6135
|
-
case "ignore": {
|
|
6136
|
-
if (parsed.operation === "get") {
|
|
6137
|
-
return ignoreTools.getIgnoreFile.execute();
|
|
6138
|
-
}
|
|
6139
|
-
if (parsed.operation === "put") {
|
|
6140
|
-
if (!parsed.content) {
|
|
6141
|
-
throw new Error("content is required for ignore put operation");
|
|
6142
|
-
}
|
|
6143
|
-
return ignoreTools.putIgnoreFile.execute({ content: parsed.content });
|
|
6144
|
-
}
|
|
6145
|
-
return ignoreTools.deleteIgnoreFile.execute();
|
|
6146
|
-
}
|
|
6147
|
-
case "mcp": {
|
|
6148
|
-
if (parsed.operation === "get") {
|
|
6149
|
-
return mcpTools.getMcpFile.execute();
|
|
6150
|
-
}
|
|
6151
|
-
if (parsed.operation === "put") {
|
|
6152
|
-
if (!parsed.content) {
|
|
6153
|
-
throw new Error("content is required for mcp put operation");
|
|
6154
|
-
}
|
|
6155
|
-
return mcpTools.putMcpFile.execute({ content: parsed.content });
|
|
6156
|
-
}
|
|
6157
|
-
return mcpTools.deleteMcpFile.execute();
|
|
6158
|
-
}
|
|
6159
|
-
case "permissions": {
|
|
6160
|
-
if (parsed.operation === "get") {
|
|
6161
|
-
return permissionsTools.getPermissionsFile.execute();
|
|
6162
|
-
}
|
|
6163
|
-
if (parsed.operation === "put") {
|
|
6164
|
-
if (!parsed.content) {
|
|
6165
|
-
throw new Error("content is required for permissions put operation");
|
|
6166
|
-
}
|
|
6167
|
-
return permissionsTools.putPermissionsFile.execute({ content: parsed.content });
|
|
6168
|
-
}
|
|
6169
|
-
return permissionsTools.deletePermissionsFile.execute();
|
|
6170
|
-
}
|
|
6171
|
-
case "hooks": {
|
|
6172
|
-
if (parsed.operation === "get") {
|
|
6173
|
-
return hooksTools.getHooksFile.execute();
|
|
6174
|
-
}
|
|
6175
|
-
if (parsed.operation === "put") {
|
|
6176
|
-
if (!parsed.content) {
|
|
6177
|
-
throw new Error("content is required for hooks put operation");
|
|
6178
|
-
}
|
|
6179
|
-
return hooksTools.putHooksFile.execute({ content: parsed.content });
|
|
6180
|
-
}
|
|
6181
|
-
return hooksTools.deleteHooksFile.execute();
|
|
6182
|
-
}
|
|
6183
|
-
case "generate": {
|
|
6184
|
-
return generateTools.executeGenerate.execute(parsed.generateOptions ?? {});
|
|
6185
|
-
}
|
|
6186
|
-
case "import": {
|
|
6187
|
-
if (!parsed.importOptions) {
|
|
6188
|
-
throw new Error("importOptions is required for import feature");
|
|
6189
|
-
}
|
|
6190
|
-
return importTools.executeImport.execute(parsed.importOptions);
|
|
6191
|
-
}
|
|
6192
|
-
case "convert": {
|
|
6193
|
-
if (!parsed.convertOptions) {
|
|
6194
|
-
throw new Error("convertOptions is required for convert feature");
|
|
6195
|
-
}
|
|
6196
|
-
return convertTools.executeConvert.execute(parsed.convertOptions);
|
|
6197
|
-
}
|
|
6198
|
-
default: {
|
|
6199
|
-
throw new Error(`Unknown feature: ${parsed.feature}`);
|
|
6200
|
-
}
|
|
6395
|
+
const executor = featureExecutors[parsed.feature];
|
|
6396
|
+
if (!executor) {
|
|
6397
|
+
throw new Error(`Unknown feature: ${parsed.feature}`);
|
|
6201
6398
|
}
|
|
6399
|
+
return executor(parsed);
|
|
6202
6400
|
}
|
|
6203
6401
|
};
|
|
6204
6402
|
|
|
@@ -6417,103 +6615,150 @@ function parseSha256Sums(content) {
|
|
|
6417
6615
|
}
|
|
6418
6616
|
return result;
|
|
6419
6617
|
}
|
|
6420
|
-
|
|
6421
|
-
const { force = false, token } = options;
|
|
6422
|
-
const updateCheck = await checkForUpdate(currentVersion, token);
|
|
6423
|
-
if (!updateCheck.hasUpdate && !force) {
|
|
6424
|
-
return `Already at the latest version (${currentVersion})`;
|
|
6425
|
-
}
|
|
6618
|
+
function resolveUpdateAssets(release) {
|
|
6426
6619
|
const assetName = getPlatformAssetName();
|
|
6427
6620
|
if (!assetName) {
|
|
6428
6621
|
throw new Error(
|
|
6429
6622
|
`Unsupported platform: ${os.platform()} ${os.arch()}. Please download manually from ${RELEASES_URL}`
|
|
6430
6623
|
);
|
|
6431
6624
|
}
|
|
6432
|
-
const binaryAsset = findAsset(
|
|
6625
|
+
const binaryAsset = findAsset(release, assetName);
|
|
6433
6626
|
if (!binaryAsset) {
|
|
6434
6627
|
throw new Error(
|
|
6435
6628
|
`Binary for ${assetName} not found in release. Please download manually from ${RELEASES_URL}`
|
|
6436
6629
|
);
|
|
6437
6630
|
}
|
|
6438
|
-
const checksumAsset = findAsset(
|
|
6631
|
+
const checksumAsset = findAsset(release, "SHA256SUMS");
|
|
6439
6632
|
if (!checksumAsset) {
|
|
6440
6633
|
throw new Error(
|
|
6441
6634
|
`SHA256SUMS not found in release. Cannot verify download integrity. Please download manually from ${RELEASES_URL}`
|
|
6442
6635
|
);
|
|
6443
6636
|
}
|
|
6444
|
-
|
|
6445
|
-
|
|
6637
|
+
return { assetName, binaryAsset, checksumAsset };
|
|
6638
|
+
}
|
|
6639
|
+
async function downloadAndVerifyBinary(params) {
|
|
6640
|
+
const { tempDir, assetName, binaryAsset, checksumAsset } = params;
|
|
6641
|
+
const tempBinaryPath = path.join(tempDir, assetName);
|
|
6642
|
+
await downloadFile(binaryAsset.browser_download_url, tempBinaryPath);
|
|
6643
|
+
const checksumsPath = path.join(tempDir, "SHA256SUMS");
|
|
6644
|
+
await downloadFile(checksumAsset.browser_download_url, checksumsPath);
|
|
6645
|
+
const checksumsContent = await fs.promises.readFile(checksumsPath, "utf-8");
|
|
6646
|
+
const checksums = parseSha256Sums(checksumsContent);
|
|
6647
|
+
const expectedChecksum = checksums.get(assetName);
|
|
6648
|
+
if (!expectedChecksum) {
|
|
6649
|
+
throw new Error(
|
|
6650
|
+
`Checksum entry for "${assetName}" not found in SHA256SUMS. Cannot verify download integrity.`
|
|
6651
|
+
);
|
|
6652
|
+
}
|
|
6653
|
+
const actualChecksum = await calculateSha256(tempBinaryPath);
|
|
6654
|
+
if (actualChecksum !== expectedChecksum) {
|
|
6655
|
+
throw new Error(
|
|
6656
|
+
`Checksum verification failed. Expected: ${expectedChecksum}, Got: ${actualChecksum}. The download may be corrupted.`
|
|
6657
|
+
);
|
|
6658
|
+
}
|
|
6659
|
+
return tempBinaryPath;
|
|
6660
|
+
}
|
|
6661
|
+
async function replaceCurrentBinary(params) {
|
|
6662
|
+
const { tempBinaryPath, currentExePath, currentDir } = params;
|
|
6663
|
+
const tempInPlace = path.join(currentDir, `.rulesync-update-${crypto.randomUUID()}`);
|
|
6446
6664
|
try {
|
|
6665
|
+
await fs.promises.copyFile(tempBinaryPath, tempInPlace);
|
|
6447
6666
|
if (os.platform() !== "win32") {
|
|
6448
|
-
await fs.promises.chmod(
|
|
6667
|
+
await fs.promises.chmod(tempInPlace, 493);
|
|
6449
6668
|
}
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
const checksums = parseSha256Sums(checksumsContent);
|
|
6456
|
-
const expectedChecksum = checksums.get(assetName);
|
|
6457
|
-
if (!expectedChecksum) {
|
|
6458
|
-
throw new Error(
|
|
6459
|
-
`Checksum entry for "${assetName}" not found in SHA256SUMS. Cannot verify download integrity.`
|
|
6460
|
-
);
|
|
6669
|
+
await fs.promises.rename(tempInPlace, currentExePath);
|
|
6670
|
+
} catch {
|
|
6671
|
+
try {
|
|
6672
|
+
await fs.promises.unlink(tempInPlace);
|
|
6673
|
+
} catch {
|
|
6461
6674
|
}
|
|
6462
|
-
|
|
6463
|
-
if (
|
|
6464
|
-
|
|
6465
|
-
`Checksum verification failed. Expected: ${expectedChecksum}, Got: ${actualChecksum}. The download may be corrupted.`
|
|
6466
|
-
);
|
|
6675
|
+
await fs.promises.copyFile(tempBinaryPath, currentExePath);
|
|
6676
|
+
if (os.platform() !== "win32") {
|
|
6677
|
+
await fs.promises.chmod(currentExePath, 493);
|
|
6467
6678
|
}
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
throw
|
|
6679
|
+
}
|
|
6680
|
+
}
|
|
6681
|
+
async function installVerifiedBinary(params) {
|
|
6682
|
+
const { tempDir, tempBinaryPath, currentVersion, latestVersion } = params;
|
|
6683
|
+
const currentExePath = await fs.promises.realpath(process.execPath);
|
|
6684
|
+
const currentDir = path.dirname(currentExePath);
|
|
6685
|
+
const backupPath = path.join(tempDir, "rulesync.backup");
|
|
6686
|
+
try {
|
|
6687
|
+
await fs.promises.copyFile(currentExePath, backupPath);
|
|
6688
|
+
} catch (error) {
|
|
6689
|
+
if (isPermissionError(error)) {
|
|
6690
|
+
throw new UpdatePermissionError(
|
|
6691
|
+
`Permission denied: Cannot read ${currentExePath}. Try running with sudo.`
|
|
6692
|
+
);
|
|
6480
6693
|
}
|
|
6694
|
+
throw error;
|
|
6695
|
+
}
|
|
6696
|
+
try {
|
|
6697
|
+
await replaceCurrentBinary({ tempBinaryPath, currentExePath, currentDir });
|
|
6698
|
+
return {
|
|
6699
|
+
message: `Successfully updated from ${currentVersion} to ${latestVersion}`,
|
|
6700
|
+
restoreFailed: false
|
|
6701
|
+
};
|
|
6702
|
+
} catch (error) {
|
|
6481
6703
|
try {
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
await fs.promises.chmod(tempInPlace, 493);
|
|
6487
|
-
}
|
|
6488
|
-
await fs.promises.rename(tempInPlace, currentExePath);
|
|
6489
|
-
} catch {
|
|
6490
|
-
try {
|
|
6491
|
-
await fs.promises.unlink(tempInPlace);
|
|
6492
|
-
} catch {
|
|
6493
|
-
}
|
|
6494
|
-
await fs.promises.copyFile(tempBinaryPath, currentExePath);
|
|
6495
|
-
if (os.platform() !== "win32") {
|
|
6496
|
-
await fs.promises.chmod(currentExePath, 493);
|
|
6497
|
-
}
|
|
6498
|
-
}
|
|
6499
|
-
return `Successfully updated from ${currentVersion} to ${updateCheck.latestVersion}`;
|
|
6500
|
-
} catch (error) {
|
|
6501
|
-
try {
|
|
6502
|
-
await fs.promises.copyFile(backupPath, currentExePath);
|
|
6503
|
-
} catch {
|
|
6504
|
-
restoreFailed = true;
|
|
6505
|
-
throw new Error(
|
|
6704
|
+
await fs.promises.copyFile(backupPath, currentExePath);
|
|
6705
|
+
} catch {
|
|
6706
|
+
throw new RestoreFailedError(
|
|
6707
|
+
new Error(
|
|
6506
6708
|
`Failed to replace binary and restore failed. Backup is preserved at: ${backupPath} (in ${tempDir}). Please manually copy it to ${currentExePath}. Original error: ${error instanceof Error ? error.message : String(error)}`,
|
|
6507
6709
|
{ cause: error }
|
|
6508
|
-
)
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
)
|
|
6514
|
-
|
|
6515
|
-
|
|
6710
|
+
)
|
|
6711
|
+
);
|
|
6712
|
+
}
|
|
6713
|
+
if (isPermissionError(error)) {
|
|
6714
|
+
throw new UpdatePermissionError(
|
|
6715
|
+
`Permission denied: Cannot write to ${path.dirname(currentExePath)}. Try running with sudo.`
|
|
6716
|
+
);
|
|
6717
|
+
}
|
|
6718
|
+
throw error;
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6721
|
+
var RestoreFailedError = class extends Error {
|
|
6722
|
+
cause;
|
|
6723
|
+
constructor(cause) {
|
|
6724
|
+
super(cause.message);
|
|
6725
|
+
this.name = "RestoreFailedError";
|
|
6726
|
+
this.cause = cause;
|
|
6727
|
+
}
|
|
6728
|
+
};
|
|
6729
|
+
async function performBinaryUpdate(currentVersion, options = {}) {
|
|
6730
|
+
const { force = false, token } = options;
|
|
6731
|
+
const updateCheck = await checkForUpdate(currentVersion, token);
|
|
6732
|
+
if (!updateCheck.hasUpdate && !force) {
|
|
6733
|
+
return `Already at the latest version (${currentVersion})`;
|
|
6734
|
+
}
|
|
6735
|
+
const { assetName, binaryAsset, checksumAsset } = resolveUpdateAssets(updateCheck.release);
|
|
6736
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "rulesync-update-"));
|
|
6737
|
+
let restoreFailed = false;
|
|
6738
|
+
try {
|
|
6739
|
+
if (os.platform() !== "win32") {
|
|
6740
|
+
await fs.promises.chmod(tempDir, 448);
|
|
6741
|
+
}
|
|
6742
|
+
const tempBinaryPath = await downloadAndVerifyBinary({
|
|
6743
|
+
tempDir,
|
|
6744
|
+
assetName,
|
|
6745
|
+
binaryAsset,
|
|
6746
|
+
checksumAsset
|
|
6747
|
+
});
|
|
6748
|
+
const installed = await installVerifiedBinary({
|
|
6749
|
+
tempDir,
|
|
6750
|
+
tempBinaryPath,
|
|
6751
|
+
currentVersion,
|
|
6752
|
+
latestVersion: updateCheck.latestVersion
|
|
6753
|
+
});
|
|
6754
|
+
restoreFailed = installed.restoreFailed;
|
|
6755
|
+
return installed.message;
|
|
6756
|
+
} catch (error) {
|
|
6757
|
+
if (error instanceof RestoreFailedError) {
|
|
6758
|
+
restoreFailed = true;
|
|
6759
|
+
throw error.cause;
|
|
6516
6760
|
}
|
|
6761
|
+
throw error;
|
|
6517
6762
|
} finally {
|
|
6518
6763
|
if (!restoreFailed) {
|
|
6519
6764
|
try {
|
|
@@ -6644,7 +6889,7 @@ function wrapCommand({
|
|
|
6644
6889
|
}
|
|
6645
6890
|
|
|
6646
6891
|
// src/cli/index.ts
|
|
6647
|
-
var getVersion = () => "8.
|
|
6892
|
+
var getVersion = () => "8.32.0";
|
|
6648
6893
|
function wrapCommand2(name, errorCode, handler) {
|
|
6649
6894
|
return wrapCommand({ name, errorCode, handler, getVersion });
|
|
6650
6895
|
}
|