rulesync 8.31.0 → 9.0.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 +8 -8
- package/dist/{chunk-NAO27T2K.js → chunk-JT4BB5RE.js} +5731 -4792
- package/dist/cli/index.cjs +7477 -6234
- package/dist/cli/index.js +1127 -827
- package/dist/index.cjs +5843 -4902
- package/dist/index.d.cts +40 -27
- package/dist/index.d.ts +40 -27
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
ConsoleLogger,
|
|
17
17
|
ErrorCodes,
|
|
18
18
|
FETCH_CONCURRENCY_LIMIT,
|
|
19
|
-
GITIGNORE_DESTINATION_KEY,
|
|
20
19
|
HooksProcessor,
|
|
21
20
|
HooksProcessorToolTargetSchema,
|
|
22
21
|
IgnoreProcessor,
|
|
@@ -83,7 +82,6 @@ import {
|
|
|
83
82
|
getHomeDirectory,
|
|
84
83
|
getLocalSkillDirNames,
|
|
85
84
|
importFromTool,
|
|
86
|
-
isFeatureValueEnabled,
|
|
87
85
|
isSymlink,
|
|
88
86
|
listDirectoryFiles,
|
|
89
87
|
readFileContent,
|
|
@@ -101,7 +99,7 @@ import {
|
|
|
101
99
|
toolSkillFactories,
|
|
102
100
|
toolSubagentFactories,
|
|
103
101
|
writeFileContent
|
|
104
|
-
} from "../chunk-
|
|
102
|
+
} from "../chunk-JT4BB5RE.js";
|
|
105
103
|
|
|
106
104
|
// src/cli/index.ts
|
|
107
105
|
import { Command } from "commander";
|
|
@@ -1132,15 +1130,6 @@ async function fetchCommand(logger5, options) {
|
|
|
1132
1130
|
}
|
|
1133
1131
|
|
|
1134
1132
|
// src/cli/commands/generate.ts
|
|
1135
|
-
function sameDirSets(a, b) {
|
|
1136
|
-
const aSet = new Set(a);
|
|
1137
|
-
const bSet = new Set(b);
|
|
1138
|
-
if (aSet.size !== bSet.size) return false;
|
|
1139
|
-
for (const v of aSet) {
|
|
1140
|
-
if (!bSet.has(v)) return false;
|
|
1141
|
-
}
|
|
1142
|
-
return true;
|
|
1143
|
-
}
|
|
1144
1133
|
function logFeatureResult(logger5, params) {
|
|
1145
1134
|
const { count, paths, featureName, isPreview, modePrefix } = params;
|
|
1146
1135
|
if (count > 0) {
|
|
@@ -1154,23 +1143,50 @@ function logFeatureResult(logger5, params) {
|
|
|
1154
1143
|
}
|
|
1155
1144
|
}
|
|
1156
1145
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1146
|
+
var FEATURE_DEBUG_MESSAGES = {
|
|
1147
|
+
ignore: "Generating ignore files...",
|
|
1148
|
+
mcp: "Generating MCP files...",
|
|
1149
|
+
commands: "Generating command files...",
|
|
1150
|
+
subagents: "Generating subagent files...",
|
|
1151
|
+
skills: "Generating skill files...",
|
|
1152
|
+
hooks: "Generating hooks...",
|
|
1153
|
+
rules: "Generating rule files..."
|
|
1154
|
+
};
|
|
1155
|
+
var FEATURE_DEBUG_ORDER = [
|
|
1156
|
+
"ignore",
|
|
1157
|
+
"mcp",
|
|
1158
|
+
"commands",
|
|
1159
|
+
"subagents",
|
|
1160
|
+
"skills",
|
|
1161
|
+
"hooks",
|
|
1162
|
+
"rules"
|
|
1163
|
+
];
|
|
1164
|
+
function logFeatureDebugMessages(logger5, features) {
|
|
1165
|
+
for (const feature of FEATURE_DEBUG_ORDER) {
|
|
1166
|
+
if (features.includes(feature)) {
|
|
1167
|
+
logger5.debug(FEATURE_DEBUG_MESSAGES[feature] ?? "");
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function buildSummaryParts(result) {
|
|
1172
|
+
const summarySpecs = [
|
|
1173
|
+
{ count: result.rulesCount, label: "rules" },
|
|
1174
|
+
{ count: result.ignoreCount, label: "ignore files" },
|
|
1175
|
+
{ count: result.mcpCount, label: "MCP files" },
|
|
1176
|
+
{ count: result.commandsCount, label: "commands" },
|
|
1177
|
+
{ count: result.subagentsCount, label: "subagents" },
|
|
1178
|
+
{ count: result.skillsCount, label: "skills" },
|
|
1179
|
+
{ count: result.hooksCount, label: "hooks" },
|
|
1180
|
+
{ count: result.permissionsCount, label: "permissions" }
|
|
1181
|
+
];
|
|
1182
|
+
const parts = [];
|
|
1183
|
+
for (const { count, label } of summarySpecs) {
|
|
1184
|
+
if (count > 0) parts.push(`${count} ${label}`);
|
|
1169
1185
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
);
|
|
1186
|
+
return parts;
|
|
1187
|
+
}
|
|
1188
|
+
async function generateCommand(logger5, options) {
|
|
1189
|
+
const config = await ConfigResolver.resolve(options, { logger: logger5 });
|
|
1174
1190
|
const check = config.getCheck();
|
|
1175
1191
|
const isPreview = config.isPreviewMode();
|
|
1176
1192
|
const modePrefix = isPreview ? "[DRY RUN]" : "";
|
|
@@ -1183,27 +1199,7 @@ async function generateCommand(logger5, options) {
|
|
|
1183
1199
|
}
|
|
1184
1200
|
logger5.debug(`Output roots: ${config.getOutputRoots().join(", ")}`);
|
|
1185
1201
|
const features = config.getFeatures();
|
|
1186
|
-
|
|
1187
|
-
logger5.debug("Generating ignore files...");
|
|
1188
|
-
}
|
|
1189
|
-
if (features.includes("mcp")) {
|
|
1190
|
-
logger5.debug("Generating MCP files...");
|
|
1191
|
-
}
|
|
1192
|
-
if (features.includes("commands")) {
|
|
1193
|
-
logger5.debug("Generating command files...");
|
|
1194
|
-
}
|
|
1195
|
-
if (features.includes("subagents")) {
|
|
1196
|
-
logger5.debug("Generating subagent files...");
|
|
1197
|
-
}
|
|
1198
|
-
if (features.includes("skills")) {
|
|
1199
|
-
logger5.debug("Generating skill files...");
|
|
1200
|
-
}
|
|
1201
|
-
if (features.includes("hooks")) {
|
|
1202
|
-
logger5.debug("Generating hooks...");
|
|
1203
|
-
}
|
|
1204
|
-
if (features.includes("rules")) {
|
|
1205
|
-
logger5.debug("Generating rule files...");
|
|
1206
|
-
}
|
|
1202
|
+
logFeatureDebugMessages(logger5, features);
|
|
1207
1203
|
const result = await generate({ config, logger: logger5 });
|
|
1208
1204
|
const totalGenerated = calculateTotalCount(result);
|
|
1209
1205
|
const featureResults = {
|
|
@@ -1256,15 +1252,7 @@ async function generateCommand(logger5, options) {
|
|
|
1256
1252
|
logger5.info(`\u2713 All files are up to date (${enabledFeatures})`);
|
|
1257
1253
|
return;
|
|
1258
1254
|
}
|
|
1259
|
-
const parts =
|
|
1260
|
-
if (result.rulesCount > 0) parts.push(`${result.rulesCount} rules`);
|
|
1261
|
-
if (result.ignoreCount > 0) parts.push(`${result.ignoreCount} ignore files`);
|
|
1262
|
-
if (result.mcpCount > 0) parts.push(`${result.mcpCount} MCP files`);
|
|
1263
|
-
if (result.commandsCount > 0) parts.push(`${result.commandsCount} commands`);
|
|
1264
|
-
if (result.subagentsCount > 0) parts.push(`${result.subagentsCount} subagents`);
|
|
1265
|
-
if (result.skillsCount > 0) parts.push(`${result.skillsCount} skills`);
|
|
1266
|
-
if (result.hooksCount > 0) parts.push(`${result.hooksCount} hooks`);
|
|
1267
|
-
if (result.permissionsCount > 0) parts.push(`${result.permissionsCount} permissions`);
|
|
1255
|
+
const parts = buildSummaryParts(result);
|
|
1268
1256
|
if (isPreview) {
|
|
1269
1257
|
logger5.info(`${modePrefix} Would write ${totalGenerated} file(s) total (${parts.join(" + ")})`);
|
|
1270
1258
|
} else {
|
|
@@ -1337,7 +1325,6 @@ var getProcessorRegistryEntry = (feature) => {
|
|
|
1337
1325
|
// src/cli/commands/gitignore-derive.ts
|
|
1338
1326
|
var TARGETS_NOT_DERIVED = /* @__PURE__ */ new Set([
|
|
1339
1327
|
"agentsskills",
|
|
1340
|
-
"antigravity",
|
|
1341
1328
|
"augmentcode-legacy",
|
|
1342
1329
|
"claudecode-legacy"
|
|
1343
1330
|
]);
|
|
@@ -1525,28 +1512,12 @@ var getSelectedGitignoreEntryTargets = (target, selectedTargets) => {
|
|
|
1525
1512
|
}
|
|
1526
1513
|
return targets.filter((candidate) => selectedTargets.includes(candidate));
|
|
1527
1514
|
};
|
|
1528
|
-
var
|
|
1515
|
+
var isFeatureSelected = (feature, features) => {
|
|
1529
1516
|
if (feature === "general") return true;
|
|
1530
1517
|
if (!features) return true;
|
|
1531
|
-
if (
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
return features.includes(feature);
|
|
1535
|
-
}
|
|
1536
|
-
if (target === "common") return true;
|
|
1537
|
-
const targetFeatures = features[target];
|
|
1538
|
-
if (!targetFeatures) return true;
|
|
1539
|
-
if (Array.isArray(targetFeatures)) {
|
|
1540
|
-
if (targetFeatures.includes("*")) return true;
|
|
1541
|
-
return targetFeatures.includes(feature);
|
|
1542
|
-
}
|
|
1543
|
-
if (isFeatureValueEnabled(targetFeatures["*"])) return true;
|
|
1544
|
-
return isFeatureValueEnabled(targetFeatures[feature]);
|
|
1545
|
-
};
|
|
1546
|
-
var isFeatureSelected = (feature, target, features) => {
|
|
1547
|
-
return normalizeGitignoreEntryTargets(target).some(
|
|
1548
|
-
(candidate) => isFeatureSelectedForTarget(feature, candidate, features)
|
|
1549
|
-
);
|
|
1518
|
+
if (features.length === 0) return true;
|
|
1519
|
+
if (features.includes("*")) return true;
|
|
1520
|
+
return features.includes(feature);
|
|
1550
1521
|
};
|
|
1551
1522
|
var warnInvalidTargets = (targets, logger5) => {
|
|
1552
1523
|
const validTargets = new Set(ALL_TOOL_TARGETS_WITH_WILDCARD);
|
|
@@ -1561,32 +1532,13 @@ var warnInvalidTargets = (targets, logger5) => {
|
|
|
1561
1532
|
var warnInvalidFeatures = (features, logger5) => {
|
|
1562
1533
|
const validFeatures = new Set(ALL_FEATURES_WITH_WILDCARD);
|
|
1563
1534
|
const warned = /* @__PURE__ */ new Set();
|
|
1564
|
-
const
|
|
1535
|
+
for (const feature of features) {
|
|
1565
1536
|
if (!validFeatures.has(feature) && !warned.has(feature)) {
|
|
1566
1537
|
warned.add(feature);
|
|
1567
1538
|
logger5?.warn(
|
|
1568
1539
|
`Unknown feature '${feature}'. Valid features: ${ALL_FEATURES_WITH_WILDCARD.join(", ")}`
|
|
1569
1540
|
);
|
|
1570
1541
|
}
|
|
1571
|
-
};
|
|
1572
|
-
if (Array.isArray(features)) {
|
|
1573
|
-
for (const feature of features) {
|
|
1574
|
-
warnOnce(feature);
|
|
1575
|
-
}
|
|
1576
|
-
} else {
|
|
1577
|
-
for (const targetFeatures of Object.values(features)) {
|
|
1578
|
-
if (!targetFeatures) continue;
|
|
1579
|
-
if (Array.isArray(targetFeatures)) {
|
|
1580
|
-
for (const feature of targetFeatures) {
|
|
1581
|
-
warnOnce(feature);
|
|
1582
|
-
}
|
|
1583
|
-
} else {
|
|
1584
|
-
for (const feature of Object.keys(targetFeatures)) {
|
|
1585
|
-
if (feature === GITIGNORE_DESTINATION_KEY) continue;
|
|
1586
|
-
warnOnce(feature);
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
1542
|
}
|
|
1591
1543
|
};
|
|
1592
1544
|
var resolveGitignoreEntries = (params) => {
|
|
@@ -1602,7 +1554,7 @@ var resolveGitignoreEntries = (params) => {
|
|
|
1602
1554
|
for (const tag of GITIGNORE_ENTRY_REGISTRY) {
|
|
1603
1555
|
if (!isTargetSelected(tag.target, targets)) continue;
|
|
1604
1556
|
const selectedTagTargets = getSelectedGitignoreEntryTargets(tag.target, targets);
|
|
1605
|
-
if (!isFeatureSelected(tag.feature,
|
|
1557
|
+
if (!isFeatureSelected(tag.feature, features)) continue;
|
|
1606
1558
|
if (seen.has(tag.entry)) continue;
|
|
1607
1559
|
seen.add(tag.entry);
|
|
1608
1560
|
result.push({
|
|
@@ -1794,9 +1746,9 @@ ${rulesyncBlock}
|
|
|
1794
1746
|
"\u{1F4A1} If you're using Google Antigravity, note that rules, workflows, and skills won't load if they're gitignored."
|
|
1795
1747
|
);
|
|
1796
1748
|
logger5.info(" You can add the following to .git/info/exclude instead:");
|
|
1797
|
-
logger5.info(" **/.
|
|
1798
|
-
logger5.info(" **/.
|
|
1799
|
-
logger5.info(" **/.
|
|
1749
|
+
logger5.info(" **/.agents/rules/");
|
|
1750
|
+
logger5.info(" **/.agents/workflows/");
|
|
1751
|
+
logger5.info(" **/.agents/skills/");
|
|
1800
1752
|
logger5.info(" For more details: https://github.com/dyoshikawa/rulesync/issues/981");
|
|
1801
1753
|
};
|
|
1802
1754
|
|
|
@@ -2211,7 +2163,7 @@ function findApmLockDependency(lock, repoUrl) {
|
|
|
2211
2163
|
|
|
2212
2164
|
// src/lib/apm/apm-manifest.ts
|
|
2213
2165
|
import { join as join5 } from "path";
|
|
2214
|
-
import {
|
|
2166
|
+
import { load as load2 } from "js-yaml";
|
|
2215
2167
|
import { optional as optional2, z as z5 } from "zod/mini";
|
|
2216
2168
|
var APM_MANIFEST_FILE_NAME = "apm.yml";
|
|
2217
2169
|
var ApmObjectDependencySchema = z5.looseObject({
|
|
@@ -2419,34 +2371,7 @@ async function installApm(params) {
|
|
|
2419
2371
|
}
|
|
2420
2372
|
const existingLock = await readApmLock(projectRoot);
|
|
2421
2373
|
if (options.frozen) {
|
|
2422
|
-
|
|
2423
|
-
throw new Error(
|
|
2424
|
-
"Frozen install failed: rulesync-apm.lock.yaml is missing. Run 'rulesync install --mode apm' to create it."
|
|
2425
|
-
);
|
|
2426
|
-
}
|
|
2427
|
-
const missing = manifest.dependencies.filter(
|
|
2428
|
-
(dep) => !findApmLockDependency(existingLock, canonicalRepoUrl(dep))
|
|
2429
|
-
);
|
|
2430
|
-
if (missing.length > 0) {
|
|
2431
|
-
const names = missing.map((d) => d.gitUrl).join(", ");
|
|
2432
|
-
throw new Error(
|
|
2433
|
-
`Frozen install failed: rulesync-apm.lock.yaml is missing entries for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
|
|
2434
|
-
);
|
|
2435
|
-
}
|
|
2436
|
-
const drifted = manifest.dependencies.filter((dep) => {
|
|
2437
|
-
if (dep.ref === void 0) return false;
|
|
2438
|
-
const locked = findApmLockDependency(existingLock, canonicalRepoUrl(dep));
|
|
2439
|
-
return locked?.resolved_ref !== void 0 && locked.resolved_ref !== dep.ref;
|
|
2440
|
-
});
|
|
2441
|
-
if (drifted.length > 0) {
|
|
2442
|
-
const names = drifted.map((d) => {
|
|
2443
|
-
const locked = findApmLockDependency(existingLock, canonicalRepoUrl(d));
|
|
2444
|
-
return `${d.gitUrl} (manifest=${d.ref}, lock=${locked?.resolved_ref})`;
|
|
2445
|
-
}).join(", ");
|
|
2446
|
-
throw new Error(
|
|
2447
|
-
`Frozen install failed: manifest ref does not match rulesync-apm.lock.yaml for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
|
|
2448
|
-
);
|
|
2449
|
-
}
|
|
2374
|
+
assertFrozenLockCoversManifest({ existingLock, dependencies: manifest.dependencies });
|
|
2450
2375
|
}
|
|
2451
2376
|
const token = GitHubClient.resolveToken(options.token);
|
|
2452
2377
|
const client = new GitHubClient({ token });
|
|
@@ -2501,30 +2426,7 @@ async function installApm(params) {
|
|
|
2501
2426
|
}
|
|
2502
2427
|
}
|
|
2503
2428
|
if (existingLock) {
|
|
2504
|
-
|
|
2505
|
-
const toDelete = [];
|
|
2506
|
-
for (const prev of existingLock.dependencies) {
|
|
2507
|
-
for (const deployed of prev.deployed_files) {
|
|
2508
|
-
if (!newDeployedFiles.has(deployed)) {
|
|
2509
|
-
toDelete.push(deployed);
|
|
2510
|
-
}
|
|
2511
|
-
}
|
|
2512
|
-
}
|
|
2513
|
-
for (const relativePath of toDelete) {
|
|
2514
|
-
if (posix2.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
|
|
2515
|
-
logger5.warn(`Refusing to remove stale apm file with suspicious path: "${relativePath}".`);
|
|
2516
|
-
continue;
|
|
2517
|
-
}
|
|
2518
|
-
try {
|
|
2519
|
-
checkPathTraversal({ relativePath, intendedRootDir: projectRoot });
|
|
2520
|
-
} catch {
|
|
2521
|
-
logger5.warn(`Refusing to remove stale apm file outside projectRoot: "${relativePath}".`);
|
|
2522
|
-
continue;
|
|
2523
|
-
}
|
|
2524
|
-
const absolute = join6(projectRoot, relativePath);
|
|
2525
|
-
await removeFile(absolute);
|
|
2526
|
-
logger5.debug(`Removed stale apm file: ${relativePath}`);
|
|
2527
|
-
}
|
|
2429
|
+
await removeStaleApmFiles({ existingLock, newLock, projectRoot, logger: logger5 });
|
|
2528
2430
|
}
|
|
2529
2431
|
if (!frozen) {
|
|
2530
2432
|
newLock.generated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -2543,6 +2445,64 @@ async function installApm(params) {
|
|
|
2543
2445
|
failedDependencyCount: failedCount
|
|
2544
2446
|
};
|
|
2545
2447
|
}
|
|
2448
|
+
function assertFrozenLockCoversManifest(params) {
|
|
2449
|
+
const { existingLock, dependencies } = params;
|
|
2450
|
+
if (!existingLock) {
|
|
2451
|
+
throw new Error(
|
|
2452
|
+
"Frozen install failed: rulesync-apm.lock.yaml is missing. Run 'rulesync install --mode apm' to create it."
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
const missing = dependencies.filter(
|
|
2456
|
+
(dep) => !findApmLockDependency(existingLock, canonicalRepoUrl(dep))
|
|
2457
|
+
);
|
|
2458
|
+
if (missing.length > 0) {
|
|
2459
|
+
const names = missing.map((d) => d.gitUrl).join(", ");
|
|
2460
|
+
throw new Error(
|
|
2461
|
+
`Frozen install failed: rulesync-apm.lock.yaml is missing entries for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
const drifted = dependencies.filter((dep) => {
|
|
2465
|
+
if (dep.ref === void 0) return false;
|
|
2466
|
+
const locked = findApmLockDependency(existingLock, canonicalRepoUrl(dep));
|
|
2467
|
+
return locked?.resolved_ref !== void 0 && locked.resolved_ref !== dep.ref;
|
|
2468
|
+
});
|
|
2469
|
+
if (drifted.length > 0) {
|
|
2470
|
+
const names = drifted.map((d) => {
|
|
2471
|
+
const locked = findApmLockDependency(existingLock, canonicalRepoUrl(d));
|
|
2472
|
+
return `${d.gitUrl} (manifest=${d.ref}, lock=${locked?.resolved_ref})`;
|
|
2473
|
+
}).join(", ");
|
|
2474
|
+
throw new Error(
|
|
2475
|
+
`Frozen install failed: manifest ref does not match rulesync-apm.lock.yaml for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
async function removeStaleApmFiles(params) {
|
|
2480
|
+
const { existingLock, newLock, projectRoot, logger: logger5 } = params;
|
|
2481
|
+
const newDeployedFiles = new Set(newLock.dependencies.flatMap((d) => d.deployed_files));
|
|
2482
|
+
const toDelete = [];
|
|
2483
|
+
for (const prev of existingLock.dependencies) {
|
|
2484
|
+
for (const deployed of prev.deployed_files) {
|
|
2485
|
+
if (!newDeployedFiles.has(deployed)) {
|
|
2486
|
+
toDelete.push(deployed);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
for (const relativePath of toDelete) {
|
|
2491
|
+
if (posix2.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
|
|
2492
|
+
logger5.warn(`Refusing to remove stale apm file with suspicious path: "${relativePath}".`);
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
try {
|
|
2496
|
+
checkPathTraversal({ relativePath, intendedRootDir: projectRoot });
|
|
2497
|
+
} catch {
|
|
2498
|
+
logger5.warn(`Refusing to remove stale apm file outside projectRoot: "${relativePath}".`);
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
const absolute = join6(projectRoot, relativePath);
|
|
2502
|
+
await removeFile(absolute);
|
|
2503
|
+
logger5.debug(`Removed stale apm file: ${relativePath}`);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2546
2506
|
async function installDependency(params) {
|
|
2547
2507
|
const { dep, client, semaphore, projectRoot, existingLock, frozen, update, logger: logger5 } = params;
|
|
2548
2508
|
const repoUrl = canonicalRepoUrl(dep);
|
|
@@ -2571,58 +2531,25 @@ async function installDependency(params) {
|
|
|
2571
2531
|
logger: logger5
|
|
2572
2532
|
});
|
|
2573
2533
|
if (files.length === 0) continue;
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
const deployRelative = toPosixPath(join6(primitive.deployDir, relativeToBase));
|
|
2589
|
-
checkPathTraversal({
|
|
2590
|
-
relativePath: deployRelative,
|
|
2591
|
-
intendedRootDir: projectRoot
|
|
2592
|
-
});
|
|
2593
|
-
const content = await withSemaphore(
|
|
2594
|
-
semaphore,
|
|
2595
|
-
() => client.getFileContent(dep.owner, dep.repo, file.path, resolvedSha)
|
|
2596
|
-
);
|
|
2597
|
-
const byteLength = Buffer.byteLength(content, "utf8");
|
|
2598
|
-
if (byteLength > MAX_FILE_SIZE) {
|
|
2599
|
-
logger5.warn(
|
|
2600
|
-
`Skipping "${file.path}" from ${repoUrl}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
2601
|
-
);
|
|
2602
|
-
continue;
|
|
2603
|
-
}
|
|
2604
|
-
deployed.push({ path: deployRelative, content });
|
|
2605
|
-
if (!frozen) {
|
|
2606
|
-
await writeFileContent(join6(projectRoot, deployRelative), content);
|
|
2607
|
-
}
|
|
2608
|
-
}
|
|
2534
|
+
await collectPrimitiveDeployments({
|
|
2535
|
+
dep,
|
|
2536
|
+
client,
|
|
2537
|
+
semaphore,
|
|
2538
|
+
projectRoot,
|
|
2539
|
+
primitive,
|
|
2540
|
+
remoteBase,
|
|
2541
|
+
files,
|
|
2542
|
+
resolvedSha,
|
|
2543
|
+
repoUrl,
|
|
2544
|
+
frozen,
|
|
2545
|
+
deployed,
|
|
2546
|
+
logger: logger5
|
|
2547
|
+
});
|
|
2609
2548
|
}
|
|
2610
2549
|
deployed.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
2611
2550
|
const deployedFiles = deployed.map((d) => d.path);
|
|
2612
2551
|
const contentHash = computeContentHash(deployed);
|
|
2613
|
-
|
|
2614
|
-
if (RULESYNC_CONTENT_HASH_REGEX.test(locked.content_hash)) {
|
|
2615
|
-
if (locked.content_hash !== contentHash) {
|
|
2616
|
-
throw new Error(
|
|
2617
|
-
`content_hash mismatch for ${repoUrl}: lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
|
|
2618
|
-
);
|
|
2619
|
-
}
|
|
2620
|
-
} else {
|
|
2621
|
-
logger5.debug(
|
|
2622
|
-
`Skipping content_hash integrity check for ${repoUrl}: recorded hash "${locked.content_hash}" was not written by rulesync.`
|
|
2623
|
-
);
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2552
|
+
assertFrozenContentHashMatches({ frozen, locked, contentHash, repoUrl, logger: logger5 });
|
|
2626
2553
|
if (frozen) {
|
|
2627
2554
|
for (const { path: deployRelative, content } of deployed) {
|
|
2628
2555
|
await writeFileContent(join6(projectRoot, deployRelative), content);
|
|
@@ -2643,6 +2570,71 @@ async function installDependency(params) {
|
|
|
2643
2570
|
logger5.info(`Installed ${deployedFiles.length} file(s) from ${repoUrl}@${shortSha(resolvedSha)}`);
|
|
2644
2571
|
return { lockEntry, deployedFiles };
|
|
2645
2572
|
}
|
|
2573
|
+
async function collectPrimitiveDeployments(params) {
|
|
2574
|
+
const {
|
|
2575
|
+
dep,
|
|
2576
|
+
client,
|
|
2577
|
+
semaphore,
|
|
2578
|
+
projectRoot,
|
|
2579
|
+
primitive,
|
|
2580
|
+
remoteBase,
|
|
2581
|
+
files,
|
|
2582
|
+
resolvedSha,
|
|
2583
|
+
repoUrl,
|
|
2584
|
+
frozen,
|
|
2585
|
+
deployed,
|
|
2586
|
+
logger: logger5
|
|
2587
|
+
} = params;
|
|
2588
|
+
for (const file of files) {
|
|
2589
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
2590
|
+
logger5.warn(
|
|
2591
|
+
`Skipping "${file.path}" from ${repoUrl}: ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
2592
|
+
);
|
|
2593
|
+
continue;
|
|
2594
|
+
}
|
|
2595
|
+
const relativeToBase = posix2.relative(remoteBase, toPosixPath(file.path));
|
|
2596
|
+
if (!relativeToBase || relativeToBase.startsWith("..") || posix2.isAbsolute(relativeToBase)) {
|
|
2597
|
+
logger5.warn(`Skipping "${file.path}" from ${repoUrl}: resolved outside of "${remoteBase}".`);
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
const deployRelative = toPosixPath(join6(primitive.deployDir, relativeToBase));
|
|
2601
|
+
checkPathTraversal({
|
|
2602
|
+
relativePath: deployRelative,
|
|
2603
|
+
intendedRootDir: projectRoot
|
|
2604
|
+
});
|
|
2605
|
+
const content = await withSemaphore(
|
|
2606
|
+
semaphore,
|
|
2607
|
+
() => client.getFileContent(dep.owner, dep.repo, file.path, resolvedSha)
|
|
2608
|
+
);
|
|
2609
|
+
const byteLength = Buffer.byteLength(content, "utf8");
|
|
2610
|
+
if (byteLength > MAX_FILE_SIZE) {
|
|
2611
|
+
logger5.warn(
|
|
2612
|
+
`Skipping "${file.path}" from ${repoUrl}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
2613
|
+
);
|
|
2614
|
+
continue;
|
|
2615
|
+
}
|
|
2616
|
+
deployed.push({ path: deployRelative, content });
|
|
2617
|
+
if (!frozen) {
|
|
2618
|
+
await writeFileContent(join6(projectRoot, deployRelative), content);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
function assertFrozenContentHashMatches(params) {
|
|
2623
|
+
const { frozen, locked, contentHash, repoUrl, logger: logger5 } = params;
|
|
2624
|
+
if (frozen && locked?.content_hash) {
|
|
2625
|
+
if (RULESYNC_CONTENT_HASH_REGEX.test(locked.content_hash)) {
|
|
2626
|
+
if (locked.content_hash !== contentHash) {
|
|
2627
|
+
throw new Error(
|
|
2628
|
+
`content_hash mismatch for ${repoUrl}: lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
|
|
2629
|
+
);
|
|
2630
|
+
}
|
|
2631
|
+
} else {
|
|
2632
|
+
logger5.debug(
|
|
2633
|
+
`Skipping content_hash integrity check for ${repoUrl}: recorded hash "${locked.content_hash}" was not written by rulesync.`
|
|
2634
|
+
);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2646
2638
|
function computeContentHash(files) {
|
|
2647
2639
|
const hash = createHash("sha256");
|
|
2648
2640
|
for (const { path: path2, content } of files) {
|
|
@@ -2685,7 +2677,7 @@ import { basename, join as join9, posix as posix3 } from "path";
|
|
|
2685
2677
|
import { Semaphore as Semaphore3 } from "es-toolkit/promise";
|
|
2686
2678
|
|
|
2687
2679
|
// src/lib/gh/gh-frontmatter.ts
|
|
2688
|
-
import { dump as
|
|
2680
|
+
import { dump as dump2, load as load3 } from "js-yaml";
|
|
2689
2681
|
var FRONTMATTER_FENCE = "---";
|
|
2690
2682
|
function injectSourceMetadata(params) {
|
|
2691
2683
|
const { content, source, repository, ref } = params;
|
|
@@ -2700,7 +2692,7 @@ function injectSourceMetadata(params) {
|
|
|
2700
2692
|
} else if (content === FRONTMATTER_FENCE) {
|
|
2701
2693
|
openFenceLen = 3;
|
|
2702
2694
|
} else {
|
|
2703
|
-
const yaml2 =
|
|
2695
|
+
const yaml2 = dump2(provenance, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2704
2696
|
return `${FRONTMATTER_FENCE}
|
|
2705
2697
|
${yaml2}${FRONTMATTER_FENCE}
|
|
2706
2698
|
${content}`;
|
|
@@ -2727,7 +2719,7 @@ ${content}`;
|
|
|
2727
2719
|
throw new Error("invalid frontmatter");
|
|
2728
2720
|
}
|
|
2729
2721
|
if (loaded === null || loaded === void 0) {
|
|
2730
|
-
const yaml2 =
|
|
2722
|
+
const yaml2 = dump2(provenance, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2731
2723
|
return `${FRONTMATTER_FENCE}
|
|
2732
2724
|
${yaml2}${FRONTMATTER_FENCE}
|
|
2733
2725
|
${rest}`;
|
|
@@ -2740,7 +2732,7 @@ ${rest}`;
|
|
|
2740
2732
|
...existing,
|
|
2741
2733
|
...provenance
|
|
2742
2734
|
};
|
|
2743
|
-
const yaml =
|
|
2735
|
+
const yaml = dump2(merged, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2744
2736
|
return `${FRONTMATTER_FENCE}
|
|
2745
2737
|
${yaml}${FRONTMATTER_FENCE}
|
|
2746
2738
|
${rest}`;
|
|
@@ -2748,7 +2740,7 @@ ${rest}`;
|
|
|
2748
2740
|
|
|
2749
2741
|
// src/lib/gh/gh-lock.ts
|
|
2750
2742
|
import { join as join7 } from "path";
|
|
2751
|
-
import { dump as
|
|
2743
|
+
import { dump as dump3, load as load4 } from "js-yaml";
|
|
2752
2744
|
import { optional as optional3, refine as refine2, z as z6 } from "zod/mini";
|
|
2753
2745
|
var GH_LOCKFILE_FILE_NAME = "rulesync-gh.lock.yaml";
|
|
2754
2746
|
var GH_LOCKFILE_VERSION = "1";
|
|
@@ -2820,7 +2812,7 @@ async function writeGhLock(params) {
|
|
|
2820
2812
|
await writeFileContent(path2, content);
|
|
2821
2813
|
}
|
|
2822
2814
|
function serializeGhLock(lock) {
|
|
2823
|
-
return
|
|
2815
|
+
return dump3(lock, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2824
2816
|
}
|
|
2825
2817
|
function findGhLockInstallation(lock, params) {
|
|
2826
2818
|
const target = params.source.toLowerCase();
|
|
@@ -2871,39 +2863,7 @@ async function installGh(params) {
|
|
|
2871
2863
|
if (sources.length === 0) {
|
|
2872
2864
|
return { sourcesProcessed: 0, installedSkillCount: 0, failedSourceCount: 0 };
|
|
2873
2865
|
}
|
|
2874
|
-
const resolvedSources = sources.map(
|
|
2875
|
-
const parsed = parseSource(entry.source);
|
|
2876
|
-
if (parsed.provider !== "github") {
|
|
2877
|
-
throw new Error(
|
|
2878
|
-
`--mode gh only supports GitHub sources. "${entry.source}" resolves to provider "${parsed.provider}".`
|
|
2879
|
-
);
|
|
2880
|
-
}
|
|
2881
|
-
if (entry.transport !== void 0 && entry.transport !== "github") {
|
|
2882
|
-
throw new Error(
|
|
2883
|
-
`--mode gh: field "transport" is not supported (got "${entry.transport}" for source "${entry.source}"). Drop the field or switch to --mode rulesync.`
|
|
2884
|
-
);
|
|
2885
|
-
}
|
|
2886
|
-
if (entry.path !== void 0) {
|
|
2887
|
-
throw new Error(
|
|
2888
|
-
`--mode gh: field "path" is not supported for source "${entry.source}". The remote layout is fixed to "skills/<name>/SKILL.md".`
|
|
2889
|
-
);
|
|
2890
|
-
}
|
|
2891
|
-
const agent = entry.agent ?? "github-copilot";
|
|
2892
|
-
if (!GH_AGENTS.includes(agent)) {
|
|
2893
|
-
throw new Error(
|
|
2894
|
-
`--mode gh: unknown agent "${agent}" for source "${entry.source}". Valid agents: ${GH_AGENTS.join(", ")}.`
|
|
2895
|
-
);
|
|
2896
|
-
}
|
|
2897
|
-
const scope = entry.scope ?? "project";
|
|
2898
|
-
return {
|
|
2899
|
-
entry,
|
|
2900
|
-
owner: parsed.owner,
|
|
2901
|
-
repo: parsed.repo,
|
|
2902
|
-
ref: entry.ref ?? parsed.ref,
|
|
2903
|
-
agent,
|
|
2904
|
-
scope
|
|
2905
|
-
};
|
|
2906
|
-
});
|
|
2866
|
+
const resolvedSources = sources.map(resolveGhSource);
|
|
2907
2867
|
const existingLock = await readGhLock(projectRoot);
|
|
2908
2868
|
const frozen = options.frozen ?? false;
|
|
2909
2869
|
const update = options.update ?? false;
|
|
@@ -2913,38 +2873,7 @@ async function installGh(params) {
|
|
|
2913
2873
|
);
|
|
2914
2874
|
}
|
|
2915
2875
|
if (frozen && existingLock) {
|
|
2916
|
-
|
|
2917
|
-
for (const rs of resolvedSources) {
|
|
2918
|
-
const hasAny = existingLock.installations.some(
|
|
2919
|
-
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase() && i.agent === rs.agent && i.scope === rs.scope
|
|
2920
|
-
);
|
|
2921
|
-
if (!hasAny) {
|
|
2922
|
-
uncovered.push(`${rs.entry.source} (agent=${rs.agent}, scope=${rs.scope})`);
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
if (uncovered.length > 0) {
|
|
2926
|
-
throw new Error(
|
|
2927
|
-
`Frozen install failed: rulesync-gh.lock.yaml is missing entries for: ${uncovered.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
2928
|
-
);
|
|
2929
|
-
}
|
|
2930
|
-
const drifted = [];
|
|
2931
|
-
for (const rs of resolvedSources) {
|
|
2932
|
-
if (!rs.ref) continue;
|
|
2933
|
-
const matches = existingLock.installations.filter(
|
|
2934
|
-
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase()
|
|
2935
|
-
);
|
|
2936
|
-
for (const m of matches) {
|
|
2937
|
-
if (m.requested_ref !== void 0 && m.requested_ref !== rs.ref) {
|
|
2938
|
-
drifted.push(`${rs.entry.source} (manifest=${rs.ref}, lock=${m.requested_ref})`);
|
|
2939
|
-
break;
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
}
|
|
2943
|
-
if (drifted.length > 0) {
|
|
2944
|
-
throw new Error(
|
|
2945
|
-
`Frozen install failed: manifest ref does not match rulesync-gh.lock.yaml for: ${drifted.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
2946
|
-
);
|
|
2947
|
-
}
|
|
2876
|
+
assertFrozenLockCoversSources({ existingLock, resolvedSources });
|
|
2948
2877
|
}
|
|
2949
2878
|
const token = GitHubClient.resolveToken(options.token);
|
|
2950
2879
|
const client = new GitHubClient({ token });
|
|
@@ -2980,49 +2909,11 @@ async function installGh(params) {
|
|
|
2980
2909
|
})
|
|
2981
2910
|
);
|
|
2982
2911
|
if (frozen) {
|
|
2983
|
-
|
|
2984
|
-
if (result.status !== "ok") continue;
|
|
2985
|
-
for (const inst of result.installations) {
|
|
2986
|
-
for (const d of inst.deployed) {
|
|
2987
|
-
await writeFileContent(d.absolutePath, d.content);
|
|
2988
|
-
}
|
|
2989
|
-
}
|
|
2990
|
-
}
|
|
2991
|
-
}
|
|
2992
|
-
let totalInstalled = 0;
|
|
2993
|
-
let failedCount = 0;
|
|
2994
|
-
for (const result of results) {
|
|
2995
|
-
if (result.status === "ok") {
|
|
2996
|
-
for (const inst of result.installations) {
|
|
2997
|
-
newLock.installations.push(inst.installation);
|
|
2998
|
-
}
|
|
2999
|
-
totalInstalled += result.installations.length;
|
|
3000
|
-
} else {
|
|
3001
|
-
failedCount += 1;
|
|
3002
|
-
for (const preserved of result.preserved) {
|
|
3003
|
-
newLock.installations.push(preserved);
|
|
3004
|
-
}
|
|
3005
|
-
}
|
|
2912
|
+
await writeDeferredFrozenFiles(results);
|
|
3006
2913
|
}
|
|
2914
|
+
const { totalInstalled, failedCount } = aggregateSourceResults({ results, newLock });
|
|
3007
2915
|
if (existingLock) {
|
|
3008
|
-
|
|
3009
|
-
for (const inst of newLock.installations) {
|
|
3010
|
-
for (const file of inst.deployed_files) {
|
|
3011
|
-
newDeployed.add(`${inst.scope}::${file}`);
|
|
3012
|
-
}
|
|
3013
|
-
}
|
|
3014
|
-
for (const prev of existingLock.installations) {
|
|
3015
|
-
for (const deployed of prev.deployed_files) {
|
|
3016
|
-
const key = `${prev.scope}::${deployed}`;
|
|
3017
|
-
if (newDeployed.has(key)) continue;
|
|
3018
|
-
await removeStaleFile({
|
|
3019
|
-
relativePath: deployed,
|
|
3020
|
-
scope: prev.scope === "user" ? "user" : "project",
|
|
3021
|
-
projectRoot,
|
|
3022
|
-
logger: logger5
|
|
3023
|
-
});
|
|
3024
|
-
}
|
|
3025
|
-
}
|
|
2916
|
+
await removeStaleGhFiles({ existingLock, newLock, projectRoot, logger: logger5 });
|
|
3026
2917
|
}
|
|
3027
2918
|
if (!frozen) {
|
|
3028
2919
|
newLock.generated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3041,80 +2932,152 @@ async function installGh(params) {
|
|
|
3041
2932
|
failedSourceCount: failedCount
|
|
3042
2933
|
};
|
|
3043
2934
|
}
|
|
3044
|
-
|
|
3045
|
-
const
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
if (rs.ref) {
|
|
3051
|
-
resolvedRef = rs.ref;
|
|
3052
|
-
} else {
|
|
3053
|
-
try {
|
|
3054
|
-
const release = await client.getLatestRelease(owner, repo);
|
|
3055
|
-
resolvedRef = release.tag_name;
|
|
3056
|
-
usedTag = true;
|
|
3057
|
-
} catch (error) {
|
|
3058
|
-
if (is404(error)) {
|
|
3059
|
-
resolvedRef = await client.getDefaultBranch(owner, repo);
|
|
3060
|
-
} else {
|
|
3061
|
-
throw error;
|
|
3062
|
-
}
|
|
3063
|
-
}
|
|
2935
|
+
function resolveGhSource(entry) {
|
|
2936
|
+
const parsed = parseSource(entry.source);
|
|
2937
|
+
if (parsed.provider !== "github") {
|
|
2938
|
+
throw new Error(
|
|
2939
|
+
`--mode gh only supports GitHub sources. "${entry.source}" resolves to provider "${parsed.provider}".`
|
|
2940
|
+
);
|
|
3064
2941
|
}
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
2942
|
+
if (entry.transport !== void 0 && entry.transport !== "github") {
|
|
2943
|
+
throw new Error(
|
|
2944
|
+
`--mode gh: field "transport" is not supported (got "${entry.transport}" for source "${entry.source}"). Drop the field or switch to --mode rulesync.`
|
|
2945
|
+
);
|
|
2946
|
+
}
|
|
2947
|
+
if (entry.path !== void 0) {
|
|
2948
|
+
throw new Error(
|
|
2949
|
+
`--mode gh: field "path" is not supported for source "${entry.source}". The remote layout is fixed to "skills/<name>/SKILL.md".`
|
|
2950
|
+
);
|
|
2951
|
+
}
|
|
2952
|
+
const agent = entry.agent ?? "github-copilot";
|
|
2953
|
+
if (!GH_AGENTS.includes(agent)) {
|
|
2954
|
+
throw new Error(
|
|
2955
|
+
`--mode gh: unknown agent "${agent}" for source "${entry.source}". Valid agents: ${GH_AGENTS.join(", ")}.`
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
const scope = entry.scope ?? "project";
|
|
2959
|
+
return {
|
|
2960
|
+
entry,
|
|
2961
|
+
owner: parsed.owner,
|
|
2962
|
+
repo: parsed.repo,
|
|
2963
|
+
ref: entry.ref ?? parsed.ref,
|
|
2964
|
+
agent,
|
|
2965
|
+
scope
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
function assertFrozenLockCoversSources(params) {
|
|
2969
|
+
const { existingLock, resolvedSources } = params;
|
|
2970
|
+
const uncovered = [];
|
|
2971
|
+
for (const rs of resolvedSources) {
|
|
2972
|
+
const hasAny = existingLock.installations.some(
|
|
2973
|
+
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase() && i.agent === rs.agent && i.scope === rs.scope
|
|
2974
|
+
);
|
|
2975
|
+
if (!hasAny) {
|
|
2976
|
+
uncovered.push(`${rs.entry.source} (agent=${rs.agent}, scope=${rs.scope})`);
|
|
3074
2977
|
}
|
|
3075
|
-
throw error;
|
|
3076
2978
|
}
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
const info = await withSemaphore(
|
|
3081
|
-
semaphore,
|
|
3082
|
-
() => client.getFileInfo(owner, repo, posix3.join(sk.path, SKILL_FILE_NAME2), resolvedSha)
|
|
2979
|
+
if (uncovered.length > 0) {
|
|
2980
|
+
throw new Error(
|
|
2981
|
+
`Frozen install failed: rulesync-gh.lock.yaml is missing entries for: ${uncovered.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
3083
2982
|
);
|
|
3084
|
-
|
|
3085
|
-
|
|
2983
|
+
}
|
|
2984
|
+
const drifted = [];
|
|
2985
|
+
for (const rs of resolvedSources) {
|
|
2986
|
+
if (!rs.ref) continue;
|
|
2987
|
+
const matches = existingLock.installations.filter(
|
|
2988
|
+
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase()
|
|
2989
|
+
);
|
|
2990
|
+
for (const m of matches) {
|
|
2991
|
+
if (m.requested_ref !== void 0 && m.requested_ref !== rs.ref) {
|
|
2992
|
+
drifted.push(`${rs.entry.source} (manifest=${rs.ref}, lock=${m.requested_ref})`);
|
|
2993
|
+
break;
|
|
2994
|
+
}
|
|
3086
2995
|
}
|
|
3087
2996
|
}
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
2997
|
+
if (drifted.length > 0) {
|
|
2998
|
+
throw new Error(
|
|
2999
|
+
`Frozen install failed: manifest ref does not match rulesync-gh.lock.yaml for: ${drifted.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
async function writeDeferredFrozenFiles(results) {
|
|
3004
|
+
for (const result of results) {
|
|
3005
|
+
if (result.status !== "ok") continue;
|
|
3006
|
+
for (const inst of result.installations) {
|
|
3007
|
+
for (const d of inst.deployed) {
|
|
3008
|
+
await writeFileContent(d.absolutePath, d.content);
|
|
3096
3009
|
}
|
|
3097
3010
|
}
|
|
3098
3011
|
}
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3012
|
+
}
|
|
3013
|
+
function aggregateSourceResults(params) {
|
|
3014
|
+
const { results, newLock } = params;
|
|
3015
|
+
let totalInstalled = 0;
|
|
3016
|
+
let failedCount = 0;
|
|
3017
|
+
for (const result of results) {
|
|
3018
|
+
if (result.status === "ok") {
|
|
3019
|
+
for (const inst of result.installations) {
|
|
3020
|
+
newLock.installations.push(inst.installation);
|
|
3021
|
+
}
|
|
3022
|
+
totalInstalled += result.installations.length;
|
|
3023
|
+
} else {
|
|
3024
|
+
failedCount += 1;
|
|
3025
|
+
for (const preserved of result.preserved) {
|
|
3026
|
+
newLock.installations.push(preserved);
|
|
3110
3027
|
}
|
|
3111
3028
|
}
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3029
|
+
}
|
|
3030
|
+
return { totalInstalled, failedCount };
|
|
3031
|
+
}
|
|
3032
|
+
async function removeStaleGhFiles(params) {
|
|
3033
|
+
const { existingLock, newLock, projectRoot, logger: logger5 } = params;
|
|
3034
|
+
const newDeployed = /* @__PURE__ */ new Set();
|
|
3035
|
+
for (const inst of newLock.installations) {
|
|
3036
|
+
for (const file of inst.deployed_files) {
|
|
3037
|
+
newDeployed.add(`${inst.scope}::${file}`);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
for (const prev of existingLock.installations) {
|
|
3041
|
+
for (const deployed of prev.deployed_files) {
|
|
3042
|
+
const key = `${prev.scope}::${deployed}`;
|
|
3043
|
+
if (newDeployed.has(key)) continue;
|
|
3044
|
+
await removeStaleFile({
|
|
3045
|
+
relativePath: deployed,
|
|
3046
|
+
scope: prev.scope === "user" ? "user" : "project",
|
|
3047
|
+
projectRoot,
|
|
3048
|
+
logger: logger5
|
|
3049
|
+
});
|
|
3116
3050
|
}
|
|
3117
3051
|
}
|
|
3052
|
+
}
|
|
3053
|
+
async function installSource(params) {
|
|
3054
|
+
const { rs, client, semaphore, projectRoot, existingLock, frozen, update, logger: logger5 } = params;
|
|
3055
|
+
const { entry, owner, repo, agent, scope } = rs;
|
|
3056
|
+
const sourceKey = entry.source;
|
|
3057
|
+
const { resolvedRef, resolvedSha, usedTag } = await resolveGhRef({
|
|
3058
|
+
rs,
|
|
3059
|
+
client,
|
|
3060
|
+
owner,
|
|
3061
|
+
repo,
|
|
3062
|
+
sourceKey,
|
|
3063
|
+
logger: logger5
|
|
3064
|
+
});
|
|
3065
|
+
const validatedSkills = await discoverValidatedSkills({
|
|
3066
|
+
client,
|
|
3067
|
+
semaphore,
|
|
3068
|
+
owner,
|
|
3069
|
+
repo,
|
|
3070
|
+
resolvedSha,
|
|
3071
|
+
sourceKey,
|
|
3072
|
+
logger: logger5
|
|
3073
|
+
});
|
|
3074
|
+
if (validatedSkills === null) {
|
|
3075
|
+
return [];
|
|
3076
|
+
}
|
|
3077
|
+
const selected = selectSkills({ validatedSkills, entry, sourceKey, logger: logger5 });
|
|
3078
|
+
if (frozen && existingLock) {
|
|
3079
|
+
assertFrozenSkillCoverage({ selected, existingLock, sourceKey, agent, scope });
|
|
3080
|
+
}
|
|
3118
3081
|
const results = [];
|
|
3119
3082
|
const installRelDir = relativeInstallDirFor({ agent, scope });
|
|
3120
3083
|
const scopeRoot = scope === "user" ? getHomeDirectory() : projectRoot;
|
|
@@ -3136,79 +3099,38 @@ async function installSource(params) {
|
|
|
3136
3099
|
ref: resolvedSha,
|
|
3137
3100
|
semaphore
|
|
3138
3101
|
});
|
|
3139
|
-
const deployed =
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
checkPathTraversal({ relativePath: withinInstallDir, intendedRootDir: installAbs });
|
|
3157
|
-
let content = await withSemaphore(
|
|
3158
|
-
semaphore,
|
|
3159
|
-
() => client.getFileContent(owner, repo, file.path, resolvedSha)
|
|
3160
|
-
);
|
|
3161
|
-
const byteLength = Buffer.byteLength(content, "utf8");
|
|
3162
|
-
if (byteLength > MAX_FILE_SIZE) {
|
|
3163
|
-
logger5.warn(
|
|
3164
|
-
`Skipping "${file.path}" from ${sourceKey}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
3165
|
-
);
|
|
3166
|
-
continue;
|
|
3167
|
-
}
|
|
3168
|
-
if (basename(file.path) === SKILL_FILE_NAME2) {
|
|
3169
|
-
try {
|
|
3170
|
-
content = injectSourceMetadata({
|
|
3171
|
-
content,
|
|
3172
|
-
source: sourceUrl,
|
|
3173
|
-
repository,
|
|
3174
|
-
ref: provenanceRef
|
|
3175
|
-
});
|
|
3176
|
-
} catch {
|
|
3177
|
-
logger5.warn(
|
|
3178
|
-
`Frontmatter in ${file.path} (${sourceKey}) is invalid. Prepending a fresh provenance block.`
|
|
3179
|
-
);
|
|
3180
|
-
content = `---
|
|
3181
|
-
source: ${sourceUrl}
|
|
3182
|
-
repository: ${repository}
|
|
3183
|
-
ref: ${provenanceRef}
|
|
3184
|
-
---
|
|
3185
|
-
${content}`;
|
|
3186
|
-
}
|
|
3187
|
-
}
|
|
3188
|
-
const absolutePath = join9(scopeRoot, deployRelative);
|
|
3189
|
-
deployed.push({ relativeToScopeRoot: deployRelative, absolutePath, content });
|
|
3190
|
-
if (!frozen) {
|
|
3191
|
-
await writeFileContent(absolutePath, content);
|
|
3192
|
-
}
|
|
3193
|
-
}
|
|
3102
|
+
const deployed = await buildSkillDeployment({
|
|
3103
|
+
sk,
|
|
3104
|
+
allFiles,
|
|
3105
|
+
client,
|
|
3106
|
+
semaphore,
|
|
3107
|
+
owner,
|
|
3108
|
+
repo,
|
|
3109
|
+
resolvedSha,
|
|
3110
|
+
installRelDir,
|
|
3111
|
+
scopeRoot,
|
|
3112
|
+
sourceUrl,
|
|
3113
|
+
repository,
|
|
3114
|
+
provenanceRef,
|
|
3115
|
+
sourceKey,
|
|
3116
|
+
frozen,
|
|
3117
|
+
logger: logger5
|
|
3118
|
+
});
|
|
3194
3119
|
deployed.sort(
|
|
3195
3120
|
(a, b) => a.relativeToScopeRoot < b.relativeToScopeRoot ? -1 : a.relativeToScopeRoot > b.relativeToScopeRoot ? 1 : 0
|
|
3196
3121
|
);
|
|
3197
3122
|
const deployedFiles = deployed.map((d) => d.relativeToScopeRoot);
|
|
3198
3123
|
const contentHash = computeContentHash2(deployed);
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
);
|
|
3210
|
-
}
|
|
3211
|
-
}
|
|
3124
|
+
assertFrozenSkillIntegrity({
|
|
3125
|
+
frozen,
|
|
3126
|
+
locked,
|
|
3127
|
+
contentHash,
|
|
3128
|
+
sourceKey,
|
|
3129
|
+
skillName: sk.name,
|
|
3130
|
+
agent,
|
|
3131
|
+
scope,
|
|
3132
|
+
logger: logger5
|
|
3133
|
+
});
|
|
3212
3134
|
const installation = {
|
|
3213
3135
|
source: sourceKey,
|
|
3214
3136
|
owner,
|
|
@@ -3232,6 +3154,180 @@ ${content}`;
|
|
|
3232
3154
|
}
|
|
3233
3155
|
return results;
|
|
3234
3156
|
}
|
|
3157
|
+
async function resolveGhRef(params) {
|
|
3158
|
+
const { rs, client, owner, repo, sourceKey, logger: logger5 } = params;
|
|
3159
|
+
let resolvedRef;
|
|
3160
|
+
let usedTag = false;
|
|
3161
|
+
if (rs.ref) {
|
|
3162
|
+
resolvedRef = rs.ref;
|
|
3163
|
+
} else {
|
|
3164
|
+
try {
|
|
3165
|
+
const release = await client.getLatestRelease(owner, repo);
|
|
3166
|
+
resolvedRef = release.tag_name;
|
|
3167
|
+
usedTag = true;
|
|
3168
|
+
} catch (error) {
|
|
3169
|
+
if (is404(error)) {
|
|
3170
|
+
resolvedRef = await client.getDefaultBranch(owner, repo);
|
|
3171
|
+
} else {
|
|
3172
|
+
throw error;
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
const resolvedSha = await client.resolveRefToSha(owner, repo, resolvedRef);
|
|
3177
|
+
logger5.debug(`Resolved ${sourceKey} -> ref=${resolvedRef} sha=${resolvedSha}`);
|
|
3178
|
+
return { resolvedRef, resolvedSha, usedTag };
|
|
3179
|
+
}
|
|
3180
|
+
async function discoverValidatedSkills(params) {
|
|
3181
|
+
const { client, semaphore, owner, repo, resolvedSha, sourceKey, logger: logger5 } = params;
|
|
3182
|
+
let topLevel;
|
|
3183
|
+
try {
|
|
3184
|
+
topLevel = await client.listDirectory(owner, repo, SKILLS_REMOTE_DIR, resolvedSha);
|
|
3185
|
+
} catch (error) {
|
|
3186
|
+
if (is404(error)) {
|
|
3187
|
+
logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
|
|
3188
|
+
return null;
|
|
3189
|
+
}
|
|
3190
|
+
throw error;
|
|
3191
|
+
}
|
|
3192
|
+
const skillDirs = topLevel.filter((e) => e.type === "dir").map((e) => ({ name: e.name, path: e.path }));
|
|
3193
|
+
const validatedSkills = [];
|
|
3194
|
+
for (const sk of skillDirs) {
|
|
3195
|
+
const info = await withSemaphore(
|
|
3196
|
+
semaphore,
|
|
3197
|
+
() => client.getFileInfo(owner, repo, posix3.join(sk.path, SKILL_FILE_NAME2), resolvedSha)
|
|
3198
|
+
);
|
|
3199
|
+
if (info) {
|
|
3200
|
+
validatedSkills.push(sk);
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
return validatedSkills;
|
|
3204
|
+
}
|
|
3205
|
+
function selectSkills(params) {
|
|
3206
|
+
const { validatedSkills, entry, sourceKey, logger: logger5 } = params;
|
|
3207
|
+
if (!entry.skills || entry.skills.length === 0) {
|
|
3208
|
+
return validatedSkills;
|
|
3209
|
+
}
|
|
3210
|
+
const requested = new Set(entry.skills);
|
|
3211
|
+
const selected = validatedSkills.filter((s) => requested.has(s.name));
|
|
3212
|
+
const presentNames = new Set(validatedSkills.map((s) => s.name));
|
|
3213
|
+
for (const want of entry.skills) {
|
|
3214
|
+
if (!presentNames.has(want)) {
|
|
3215
|
+
logger5.warn(`Requested skill "${want}" not found in ${sourceKey} under skills/. Skipping.`);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
return selected;
|
|
3219
|
+
}
|
|
3220
|
+
function assertFrozenSkillCoverage(params) {
|
|
3221
|
+
const { selected, existingLock, sourceKey, agent, scope } = params;
|
|
3222
|
+
const missing = [];
|
|
3223
|
+
for (const sk of selected) {
|
|
3224
|
+
const locked = findGhLockInstallation(existingLock, {
|
|
3225
|
+
source: sourceKey,
|
|
3226
|
+
agent,
|
|
3227
|
+
scope,
|
|
3228
|
+
skill: sk.name
|
|
3229
|
+
});
|
|
3230
|
+
if (!locked) {
|
|
3231
|
+
missing.push(sk.name);
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
if (missing.length > 0) {
|
|
3235
|
+
throw new Error(
|
|
3236
|
+
`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.`
|
|
3237
|
+
);
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
async function buildSkillDeployment(params) {
|
|
3241
|
+
const {
|
|
3242
|
+
sk,
|
|
3243
|
+
allFiles,
|
|
3244
|
+
client,
|
|
3245
|
+
semaphore,
|
|
3246
|
+
owner,
|
|
3247
|
+
repo,
|
|
3248
|
+
resolvedSha,
|
|
3249
|
+
installRelDir,
|
|
3250
|
+
scopeRoot,
|
|
3251
|
+
sourceUrl,
|
|
3252
|
+
repository,
|
|
3253
|
+
provenanceRef,
|
|
3254
|
+
sourceKey,
|
|
3255
|
+
frozen,
|
|
3256
|
+
logger: logger5
|
|
3257
|
+
} = params;
|
|
3258
|
+
const deployed = [];
|
|
3259
|
+
for (const file of allFiles) {
|
|
3260
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
3261
|
+
logger5.warn(
|
|
3262
|
+
`Skipping "${file.path}" from ${sourceKey}: ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
3263
|
+
);
|
|
3264
|
+
continue;
|
|
3265
|
+
}
|
|
3266
|
+
const relativeToSkill = posix3.relative(sk.path, toPosixPath(file.path));
|
|
3267
|
+
if (!relativeToSkill || relativeToSkill.startsWith("..") || posix3.isAbsolute(relativeToSkill)) {
|
|
3268
|
+
logger5.warn(`Skipping "${file.path}" from ${sourceKey}: resolved outside of "${sk.path}".`);
|
|
3269
|
+
continue;
|
|
3270
|
+
}
|
|
3271
|
+
const deployRelative = toPosixPath(join9(installRelDir, sk.name, relativeToSkill));
|
|
3272
|
+
checkPathTraversal({ relativePath: deployRelative, intendedRootDir: scopeRoot });
|
|
3273
|
+
const installAbs = join9(scopeRoot, installRelDir);
|
|
3274
|
+
const withinInstallDir = toPosixPath(join9(sk.name, relativeToSkill));
|
|
3275
|
+
checkPathTraversal({ relativePath: withinInstallDir, intendedRootDir: installAbs });
|
|
3276
|
+
let content = await withSemaphore(
|
|
3277
|
+
semaphore,
|
|
3278
|
+
() => client.getFileContent(owner, repo, file.path, resolvedSha)
|
|
3279
|
+
);
|
|
3280
|
+
const byteLength = Buffer.byteLength(content, "utf8");
|
|
3281
|
+
if (byteLength > MAX_FILE_SIZE) {
|
|
3282
|
+
logger5.warn(
|
|
3283
|
+
`Skipping "${file.path}" from ${sourceKey}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
3284
|
+
);
|
|
3285
|
+
continue;
|
|
3286
|
+
}
|
|
3287
|
+
if (basename(file.path) === SKILL_FILE_NAME2) {
|
|
3288
|
+
try {
|
|
3289
|
+
content = injectSourceMetadata({
|
|
3290
|
+
content,
|
|
3291
|
+
source: sourceUrl,
|
|
3292
|
+
repository,
|
|
3293
|
+
ref: provenanceRef
|
|
3294
|
+
});
|
|
3295
|
+
} catch {
|
|
3296
|
+
logger5.warn(
|
|
3297
|
+
`Frontmatter in ${file.path} (${sourceKey}) is invalid. Prepending a fresh provenance block.`
|
|
3298
|
+
);
|
|
3299
|
+
content = `---
|
|
3300
|
+
source: ${sourceUrl}
|
|
3301
|
+
repository: ${repository}
|
|
3302
|
+
ref: ${provenanceRef}
|
|
3303
|
+
---
|
|
3304
|
+
${content}`;
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
const absolutePath = join9(scopeRoot, deployRelative);
|
|
3308
|
+
deployed.push({ relativeToScopeRoot: deployRelative, absolutePath, content });
|
|
3309
|
+
if (!frozen) {
|
|
3310
|
+
await writeFileContent(absolutePath, content);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
return deployed;
|
|
3314
|
+
}
|
|
3315
|
+
function assertFrozenSkillIntegrity(params) {
|
|
3316
|
+
const { frozen, locked, contentHash, sourceKey, skillName, agent, scope, logger: logger5 } = params;
|
|
3317
|
+
if (frozen && locked?.content_hash) {
|
|
3318
|
+
if (RULESYNC_CONTENT_HASH_REGEX2.test(locked.content_hash)) {
|
|
3319
|
+
if (locked.content_hash !== contentHash) {
|
|
3320
|
+
throw new Error(
|
|
3321
|
+
`content_hash mismatch for ${sourceKey} skill "${skillName}" (agent=${agent}, scope=${scope}): lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
|
|
3322
|
+
);
|
|
3323
|
+
}
|
|
3324
|
+
} else {
|
|
3325
|
+
logger5.debug(
|
|
3326
|
+
`Skipping content_hash integrity check for ${sourceKey} skill "${skillName}": recorded hash "${locked.content_hash}" was not written by rulesync.`
|
|
3327
|
+
);
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3235
3331
|
async function removeStaleFile(params) {
|
|
3236
3332
|
const { relativePath, scope, projectRoot, logger: logger5 } = params;
|
|
3237
3333
|
if (posix3.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
|
|
@@ -3623,18 +3719,7 @@ async function resolveAndFetchSources(params) {
|
|
|
3623
3719
|
}
|
|
3624
3720
|
let lock = options.updateSources ? createEmptyLock() : await readLockFile({ projectRoot, logger: logger5 });
|
|
3625
3721
|
if (options.frozen) {
|
|
3626
|
-
|
|
3627
|
-
for (const source of sources) {
|
|
3628
|
-
const locked = getLockedSource(lock, source.source);
|
|
3629
|
-
if (!locked) {
|
|
3630
|
-
missingKeys.push(source.source);
|
|
3631
|
-
}
|
|
3632
|
-
}
|
|
3633
|
-
if (missingKeys.length > 0) {
|
|
3634
|
-
throw new Error(
|
|
3635
|
-
`Frozen install failed: lockfile is missing entries for: ${missingKeys.join(", ")}. Run 'rulesync install' to update the lockfile.`
|
|
3636
|
-
);
|
|
3637
|
-
}
|
|
3722
|
+
assertFrozenLockCoversSources2({ lock, sources });
|
|
3638
3723
|
}
|
|
3639
3724
|
const originalLockJson = JSON.stringify(lock);
|
|
3640
3725
|
const token = GitHubClient.resolveToken(options.token);
|
|
@@ -3644,31 +3729,17 @@ async function resolveAndFetchSources(params) {
|
|
|
3644
3729
|
const allFetchedSkillNames = /* @__PURE__ */ new Set();
|
|
3645
3730
|
for (const sourceEntry of sources) {
|
|
3646
3731
|
try {
|
|
3647
|
-
const
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
logger: logger5
|
|
3659
|
-
});
|
|
3660
|
-
} else {
|
|
3661
|
-
result = await fetchSource({
|
|
3662
|
-
sourceEntry,
|
|
3663
|
-
client,
|
|
3664
|
-
projectRoot,
|
|
3665
|
-
lock,
|
|
3666
|
-
localSkillNames,
|
|
3667
|
-
alreadyFetchedSkillNames: allFetchedSkillNames,
|
|
3668
|
-
updateSources: options.updateSources ?? false,
|
|
3669
|
-
logger: logger5
|
|
3670
|
-
});
|
|
3671
|
-
}
|
|
3732
|
+
const result = await fetchSourceByTransport({
|
|
3733
|
+
sourceEntry,
|
|
3734
|
+
client,
|
|
3735
|
+
projectRoot,
|
|
3736
|
+
lock,
|
|
3737
|
+
localSkillNames,
|
|
3738
|
+
alreadyFetchedSkillNames: allFetchedSkillNames,
|
|
3739
|
+
updateSources: options.updateSources ?? false,
|
|
3740
|
+
frozen: options.frozen ?? false,
|
|
3741
|
+
logger: logger5
|
|
3742
|
+
});
|
|
3672
3743
|
const { skillCount, fetchedSkillNames, updatedLock } = result;
|
|
3673
3744
|
lock = updatedLock;
|
|
3674
3745
|
totalSkillCount += skillCount;
|
|
@@ -3684,16 +3755,7 @@ async function resolveAndFetchSources(params) {
|
|
|
3684
3755
|
}
|
|
3685
3756
|
}
|
|
3686
3757
|
}
|
|
3687
|
-
|
|
3688
|
-
const prunedSources = {};
|
|
3689
|
-
for (const [key, value] of Object.entries(lock.sources)) {
|
|
3690
|
-
if (sourceKeys.has(normalizeSourceKey(key))) {
|
|
3691
|
-
prunedSources[key] = value;
|
|
3692
|
-
} else {
|
|
3693
|
-
logger5.debug(`Pruned stale lockfile entry: ${key}`);
|
|
3694
|
-
}
|
|
3695
|
-
}
|
|
3696
|
-
lock = { lockfileVersion: lock.lockfileVersion, sources: prunedSources };
|
|
3758
|
+
lock = pruneStaleLockEntries({ lock, sources, logger: logger5 });
|
|
3697
3759
|
if (!options.frozen && JSON.stringify(lock) !== originalLockJson) {
|
|
3698
3760
|
await writeLockFile({ projectRoot, lock, logger: logger5 });
|
|
3699
3761
|
} else {
|
|
@@ -3709,6 +3771,70 @@ function logGitClientHints(params) {
|
|
|
3709
3771
|
logger5.info("Hint: Check your git credentials (SSH keys, credential helper, or access token).");
|
|
3710
3772
|
}
|
|
3711
3773
|
}
|
|
3774
|
+
function assertFrozenLockCoversSources2(params) {
|
|
3775
|
+
const { lock, sources } = params;
|
|
3776
|
+
const missingKeys = [];
|
|
3777
|
+
for (const source of sources) {
|
|
3778
|
+
const locked = getLockedSource(lock, source.source);
|
|
3779
|
+
if (!locked) {
|
|
3780
|
+
missingKeys.push(source.source);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
if (missingKeys.length > 0) {
|
|
3784
|
+
throw new Error(
|
|
3785
|
+
`Frozen install failed: lockfile is missing entries for: ${missingKeys.join(", ")}. Run 'rulesync install' to update the lockfile.`
|
|
3786
|
+
);
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
async function fetchSourceByTransport(params) {
|
|
3790
|
+
const {
|
|
3791
|
+
sourceEntry,
|
|
3792
|
+
client,
|
|
3793
|
+
projectRoot,
|
|
3794
|
+
lock,
|
|
3795
|
+
localSkillNames,
|
|
3796
|
+
alreadyFetchedSkillNames,
|
|
3797
|
+
updateSources,
|
|
3798
|
+
frozen,
|
|
3799
|
+
logger: logger5
|
|
3800
|
+
} = params;
|
|
3801
|
+
const transport = sourceEntry.transport ?? "github";
|
|
3802
|
+
if (transport === "git") {
|
|
3803
|
+
return fetchSourceViaGit({
|
|
3804
|
+
sourceEntry,
|
|
3805
|
+
projectRoot,
|
|
3806
|
+
lock,
|
|
3807
|
+
localSkillNames,
|
|
3808
|
+
alreadyFetchedSkillNames,
|
|
3809
|
+
updateSources,
|
|
3810
|
+
frozen,
|
|
3811
|
+
logger: logger5
|
|
3812
|
+
});
|
|
3813
|
+
}
|
|
3814
|
+
return fetchSource({
|
|
3815
|
+
sourceEntry,
|
|
3816
|
+
client,
|
|
3817
|
+
projectRoot,
|
|
3818
|
+
lock,
|
|
3819
|
+
localSkillNames,
|
|
3820
|
+
alreadyFetchedSkillNames,
|
|
3821
|
+
updateSources,
|
|
3822
|
+
logger: logger5
|
|
3823
|
+
});
|
|
3824
|
+
}
|
|
3825
|
+
function pruneStaleLockEntries(params) {
|
|
3826
|
+
const { lock, sources, logger: logger5 } = params;
|
|
3827
|
+
const sourceKeys = new Set(sources.map((s) => normalizeSourceKey(s.source)));
|
|
3828
|
+
const prunedSources = {};
|
|
3829
|
+
for (const [key, value] of Object.entries(lock.sources)) {
|
|
3830
|
+
if (sourceKeys.has(normalizeSourceKey(key))) {
|
|
3831
|
+
prunedSources[key] = value;
|
|
3832
|
+
} else {
|
|
3833
|
+
logger5.debug(`Pruned stale lockfile entry: ${key}`);
|
|
3834
|
+
}
|
|
3835
|
+
}
|
|
3836
|
+
return { lockfileVersion: lock.lockfileVersion, sources: prunedSources };
|
|
3837
|
+
}
|
|
3712
3838
|
async function checkLockedSkillsExist(curatedDir, skillNames) {
|
|
3713
3839
|
if (skillNames.length === 0) return true;
|
|
3714
3840
|
for (const name of skillNames) {
|
|
@@ -3840,7 +3966,189 @@ function groupRemoteFilesBySkillRoot(params) {
|
|
|
3840
3966
|
grouped.set(singleSkillName, rootLevelFiles);
|
|
3841
3967
|
}
|
|
3842
3968
|
}
|
|
3843
|
-
return grouped;
|
|
3969
|
+
return grouped;
|
|
3970
|
+
}
|
|
3971
|
+
async function resolveGithubFetchRef(params) {
|
|
3972
|
+
const { parsed, locked, updateSources, sourceKey, client, logger: logger5 } = params;
|
|
3973
|
+
if (locked && !updateSources) {
|
|
3974
|
+
logger5.debug(`Using locked ref for ${sourceKey}: ${locked.resolvedRef}`);
|
|
3975
|
+
return {
|
|
3976
|
+
ref: locked.resolvedRef,
|
|
3977
|
+
resolvedSha: locked.resolvedRef,
|
|
3978
|
+
requestedRef: locked.requestedRef
|
|
3979
|
+
};
|
|
3980
|
+
}
|
|
3981
|
+
const requestedRef = parsed.ref ?? await client.getDefaultBranch(parsed.owner, parsed.repo);
|
|
3982
|
+
const resolvedSha = await client.resolveRefToSha(parsed.owner, parsed.repo, requestedRef);
|
|
3983
|
+
logger5.debug(`Resolved ${sourceKey} ref "${requestedRef}" to SHA: ${resolvedSha}`);
|
|
3984
|
+
return { ref: resolvedSha, resolvedSha, requestedRef };
|
|
3985
|
+
}
|
|
3986
|
+
async function fetchRootLevelFallbackSkill(params) {
|
|
3987
|
+
const {
|
|
3988
|
+
entries,
|
|
3989
|
+
parsed,
|
|
3990
|
+
ref,
|
|
3991
|
+
resolvedSha,
|
|
3992
|
+
skillFilter,
|
|
3993
|
+
isWildcard,
|
|
3994
|
+
curatedDir,
|
|
3995
|
+
locked,
|
|
3996
|
+
sourceKey,
|
|
3997
|
+
localSkillNames,
|
|
3998
|
+
alreadyFetchedSkillNames,
|
|
3999
|
+
client,
|
|
4000
|
+
semaphore,
|
|
4001
|
+
fetchedSkills,
|
|
4002
|
+
logger: logger5
|
|
4003
|
+
} = params;
|
|
4004
|
+
const rootFiles = entries.filter((entry) => entry.type === "file");
|
|
4005
|
+
const rootSkillFiles = [];
|
|
4006
|
+
for (const file of rootFiles) {
|
|
4007
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
4008
|
+
logger5.warn(
|
|
4009
|
+
`Skipping file "${file.path}" (${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit).`
|
|
4010
|
+
);
|
|
4011
|
+
continue;
|
|
4012
|
+
}
|
|
4013
|
+
const content = await withSemaphore(
|
|
4014
|
+
semaphore,
|
|
4015
|
+
() => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
|
|
4016
|
+
);
|
|
4017
|
+
rootSkillFiles.push({ relativePath: file.name, content });
|
|
4018
|
+
}
|
|
4019
|
+
const groupedRootFiles = groupRemoteFilesBySkillRoot({
|
|
4020
|
+
remoteFiles: rootSkillFiles,
|
|
4021
|
+
skillFilter,
|
|
4022
|
+
isWildcard
|
|
4023
|
+
});
|
|
4024
|
+
const [fallbackSkillName] = groupedRootFiles.keys();
|
|
4025
|
+
if (fallbackSkillName === void 0) {
|
|
4026
|
+
return { handled: false, remoteSkillNames: [] };
|
|
4027
|
+
}
|
|
4028
|
+
if (!shouldSkipSkill({
|
|
4029
|
+
skillName: fallbackSkillName,
|
|
4030
|
+
sourceKey,
|
|
4031
|
+
localSkillNames,
|
|
4032
|
+
alreadyFetchedSkillNames,
|
|
4033
|
+
logger: logger5
|
|
4034
|
+
})) {
|
|
4035
|
+
fetchedSkills[fallbackSkillName] = await writeSkillAndComputeIntegrity({
|
|
4036
|
+
skillName: fallbackSkillName,
|
|
4037
|
+
files: groupedRootFiles.get(fallbackSkillName) ?? [],
|
|
4038
|
+
curatedDir,
|
|
4039
|
+
locked,
|
|
4040
|
+
resolvedSha,
|
|
4041
|
+
sourceKey,
|
|
4042
|
+
logger: logger5
|
|
4043
|
+
});
|
|
4044
|
+
logger5.debug(`Fetched skill "${fallbackSkillName}" from ${sourceKey}`);
|
|
4045
|
+
}
|
|
4046
|
+
return { handled: true, remoteSkillNames: [fallbackSkillName] };
|
|
4047
|
+
}
|
|
4048
|
+
async function fetchGithubSkillDir(params) {
|
|
4049
|
+
const {
|
|
4050
|
+
skillDir,
|
|
4051
|
+
parsed,
|
|
4052
|
+
ref,
|
|
4053
|
+
resolvedSha,
|
|
4054
|
+
curatedDir,
|
|
4055
|
+
locked,
|
|
4056
|
+
sourceKey,
|
|
4057
|
+
client,
|
|
4058
|
+
semaphore,
|
|
4059
|
+
logger: logger5
|
|
4060
|
+
} = params;
|
|
4061
|
+
const allFiles = await listDirectoryRecursive({
|
|
4062
|
+
client,
|
|
4063
|
+
owner: parsed.owner,
|
|
4064
|
+
repo: parsed.repo,
|
|
4065
|
+
path: skillDir.path,
|
|
4066
|
+
ref,
|
|
4067
|
+
semaphore
|
|
4068
|
+
});
|
|
4069
|
+
const files = allFiles.filter((file) => {
|
|
4070
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
4071
|
+
logger5.warn(
|
|
4072
|
+
`Skipping file "${file.path}" (${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit).`
|
|
4073
|
+
);
|
|
4074
|
+
return false;
|
|
4075
|
+
}
|
|
4076
|
+
return true;
|
|
4077
|
+
});
|
|
4078
|
+
const skillFiles = [];
|
|
4079
|
+
for (const file of files) {
|
|
4080
|
+
const relativeToSkill = file.path.substring(skillDir.path.length + 1);
|
|
4081
|
+
const content = await withSemaphore(
|
|
4082
|
+
semaphore,
|
|
4083
|
+
() => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
|
|
4084
|
+
);
|
|
4085
|
+
skillFiles.push({ relativePath: relativeToSkill, content });
|
|
4086
|
+
}
|
|
4087
|
+
return writeSkillAndComputeIntegrity({
|
|
4088
|
+
skillName: skillDir.name,
|
|
4089
|
+
files: skillFiles,
|
|
4090
|
+
curatedDir,
|
|
4091
|
+
locked,
|
|
4092
|
+
resolvedSha,
|
|
4093
|
+
sourceKey,
|
|
4094
|
+
logger: logger5
|
|
4095
|
+
});
|
|
4096
|
+
}
|
|
4097
|
+
async function discoverGithubSkillDirs(params) {
|
|
4098
|
+
const {
|
|
4099
|
+
parsed,
|
|
4100
|
+
ref,
|
|
4101
|
+
resolvedSha,
|
|
4102
|
+
skillFilter,
|
|
4103
|
+
isWildcard,
|
|
4104
|
+
curatedDir,
|
|
4105
|
+
locked,
|
|
4106
|
+
sourceKey,
|
|
4107
|
+
localSkillNames,
|
|
4108
|
+
alreadyFetchedSkillNames,
|
|
4109
|
+
client,
|
|
4110
|
+
semaphore,
|
|
4111
|
+
fetchedSkills,
|
|
4112
|
+
logger: logger5
|
|
4113
|
+
} = params;
|
|
4114
|
+
const skillsBasePath = parsed.path ?? "skills";
|
|
4115
|
+
try {
|
|
4116
|
+
const entries = await client.listDirectory(parsed.owner, parsed.repo, skillsBasePath, ref);
|
|
4117
|
+
const remoteSkillDirs = entries.filter((e) => e.type === "dir").map((e) => ({ name: e.name, path: e.path }));
|
|
4118
|
+
if (remoteSkillDirs.length === 0 && !isWildcard && skillFilter.length === 1) {
|
|
4119
|
+
const fallback = await fetchRootLevelFallbackSkill({
|
|
4120
|
+
entries,
|
|
4121
|
+
parsed,
|
|
4122
|
+
ref,
|
|
4123
|
+
resolvedSha,
|
|
4124
|
+
skillFilter,
|
|
4125
|
+
isWildcard,
|
|
4126
|
+
curatedDir,
|
|
4127
|
+
locked,
|
|
4128
|
+
sourceKey,
|
|
4129
|
+
localSkillNames,
|
|
4130
|
+
alreadyFetchedSkillNames,
|
|
4131
|
+
client,
|
|
4132
|
+
semaphore,
|
|
4133
|
+
fetchedSkills,
|
|
4134
|
+
logger: logger5
|
|
4135
|
+
});
|
|
4136
|
+
if (fallback.handled) {
|
|
4137
|
+
return {
|
|
4138
|
+
status: "ok",
|
|
4139
|
+
remoteSkillDirs,
|
|
4140
|
+
fallbackHandled: true,
|
|
4141
|
+
remoteSkillNames: fallback.remoteSkillNames
|
|
4142
|
+
};
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
return { status: "ok", remoteSkillDirs, fallbackHandled: false, remoteSkillNames: [] };
|
|
4146
|
+
} catch (error) {
|
|
4147
|
+
if (error instanceof GitHubClientError && error.statusCode === 404) {
|
|
4148
|
+
return { status: "notFound" };
|
|
4149
|
+
}
|
|
4150
|
+
throw error;
|
|
4151
|
+
}
|
|
3844
4152
|
}
|
|
3845
4153
|
async function fetchSource(params) {
|
|
3846
4154
|
const {
|
|
@@ -3861,20 +4169,14 @@ async function fetchSource(params) {
|
|
|
3861
4169
|
const sourceKey = sourceEntry.source;
|
|
3862
4170
|
const locked = getLockedSource(lock, sourceKey);
|
|
3863
4171
|
const lockedSkillNames = locked ? getLockedSkillNames(locked) : [];
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
} else {
|
|
3873
|
-
requestedRef = parsed.ref ?? await client.getDefaultBranch(parsed.owner, parsed.repo);
|
|
3874
|
-
resolvedSha = await client.resolveRefToSha(parsed.owner, parsed.repo, requestedRef);
|
|
3875
|
-
ref = resolvedSha;
|
|
3876
|
-
logger5.debug(`Resolved ${sourceKey} ref "${requestedRef}" to SHA: ${resolvedSha}`);
|
|
3877
|
-
}
|
|
4172
|
+
const { ref, resolvedSha, requestedRef } = await resolveGithubFetchRef({
|
|
4173
|
+
parsed,
|
|
4174
|
+
locked,
|
|
4175
|
+
updateSources,
|
|
4176
|
+
sourceKey,
|
|
4177
|
+
client,
|
|
4178
|
+
logger: logger5
|
|
4179
|
+
});
|
|
3878
4180
|
const curatedDir = join12(projectRoot, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
|
|
3879
4181
|
if (locked && resolvedSha === locked.resolvedRef && !updateSources) {
|
|
3880
4182
|
const allExist = await checkLockedSkillsExist(curatedDir, lockedSkillNames);
|
|
@@ -3891,69 +4193,29 @@ async function fetchSource(params) {
|
|
|
3891
4193
|
const isWildcard = skillFilter.length === 1 && skillFilter[0] === "*";
|
|
3892
4194
|
const semaphore = new Semaphore4(FETCH_CONCURRENCY_LIMIT);
|
|
3893
4195
|
const fetchedSkills = {};
|
|
3894
|
-
const
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
() => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
|
|
3914
|
-
);
|
|
3915
|
-
rootSkillFiles.push({ relativePath: file.name, content });
|
|
3916
|
-
}
|
|
3917
|
-
const groupedRootFiles = groupRemoteFilesBySkillRoot({
|
|
3918
|
-
remoteFiles: rootSkillFiles,
|
|
3919
|
-
skillFilter,
|
|
3920
|
-
isWildcard
|
|
3921
|
-
});
|
|
3922
|
-
const [fallbackSkillName] = groupedRootFiles.keys();
|
|
3923
|
-
if (fallbackSkillName !== void 0) {
|
|
3924
|
-
fallbackHandled = true;
|
|
3925
|
-
remoteSkillNames = [fallbackSkillName];
|
|
3926
|
-
if (!shouldSkipSkill({
|
|
3927
|
-
skillName: fallbackSkillName,
|
|
3928
|
-
sourceKey,
|
|
3929
|
-
localSkillNames,
|
|
3930
|
-
alreadyFetchedSkillNames,
|
|
3931
|
-
logger: logger5
|
|
3932
|
-
})) {
|
|
3933
|
-
fetchedSkills[fallbackSkillName] = await writeSkillAndComputeIntegrity({
|
|
3934
|
-
skillName: fallbackSkillName,
|
|
3935
|
-
files: groupedRootFiles.get(fallbackSkillName) ?? [],
|
|
3936
|
-
curatedDir,
|
|
3937
|
-
locked,
|
|
3938
|
-
resolvedSha,
|
|
3939
|
-
sourceKey,
|
|
3940
|
-
logger: logger5
|
|
3941
|
-
});
|
|
3942
|
-
logger5.debug(`Fetched skill "${fallbackSkillName}" from ${sourceKey}`);
|
|
3943
|
-
}
|
|
3944
|
-
}
|
|
3945
|
-
}
|
|
3946
|
-
} catch (error) {
|
|
3947
|
-
if (error instanceof GitHubClientError && error.statusCode === 404) {
|
|
3948
|
-
logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
|
|
3949
|
-
return { skillCount: 0, fetchedSkillNames: [], updatedLock: lock };
|
|
3950
|
-
}
|
|
3951
|
-
throw error;
|
|
4196
|
+
const discovery = await discoverGithubSkillDirs({
|
|
4197
|
+
parsed,
|
|
4198
|
+
ref,
|
|
4199
|
+
resolvedSha,
|
|
4200
|
+
skillFilter,
|
|
4201
|
+
isWildcard,
|
|
4202
|
+
curatedDir,
|
|
4203
|
+
locked,
|
|
4204
|
+
sourceKey,
|
|
4205
|
+
localSkillNames,
|
|
4206
|
+
alreadyFetchedSkillNames,
|
|
4207
|
+
client,
|
|
4208
|
+
semaphore,
|
|
4209
|
+
fetchedSkills,
|
|
4210
|
+
logger: logger5
|
|
4211
|
+
});
|
|
4212
|
+
if (discovery.status === "notFound") {
|
|
4213
|
+
logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
|
|
4214
|
+
return { skillCount: 0, fetchedSkillNames: [], updatedLock: lock };
|
|
3952
4215
|
}
|
|
4216
|
+
const { remoteSkillDirs, fallbackHandled, remoteSkillNames: fallbackSkillNames } = discovery;
|
|
3953
4217
|
const filteredDirs = isWildcard ? remoteSkillDirs : remoteSkillDirs.filter((d) => skillFilter.includes(d.name));
|
|
3954
|
-
|
|
3955
|
-
remoteSkillNames = filteredDirs.map((d) => d.name);
|
|
3956
|
-
}
|
|
4218
|
+
const remoteSkillNames = fallbackHandled ? fallbackSkillNames : filteredDirs.map((d) => d.name);
|
|
3957
4219
|
if (locked) {
|
|
3958
4220
|
await cleanPreviousCuratedSkills({ curatedDir, lockedSkillNames, logger: logger5 });
|
|
3959
4221
|
}
|
|
@@ -3967,39 +4229,16 @@ async function fetchSource(params) {
|
|
|
3967
4229
|
})) {
|
|
3968
4230
|
continue;
|
|
3969
4231
|
}
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
repo: parsed.repo,
|
|
3974
|
-
path: skillDir.path,
|
|
4232
|
+
fetchedSkills[skillDir.name] = await fetchGithubSkillDir({
|
|
4233
|
+
skillDir,
|
|
4234
|
+
parsed,
|
|
3975
4235
|
ref,
|
|
3976
|
-
|
|
3977
|
-
});
|
|
3978
|
-
const files = allFiles.filter((file) => {
|
|
3979
|
-
if (file.size > MAX_FILE_SIZE) {
|
|
3980
|
-
logger5.warn(
|
|
3981
|
-
`Skipping file "${file.path}" (${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit).`
|
|
3982
|
-
);
|
|
3983
|
-
return false;
|
|
3984
|
-
}
|
|
3985
|
-
return true;
|
|
3986
|
-
});
|
|
3987
|
-
const skillFiles = [];
|
|
3988
|
-
for (const file of files) {
|
|
3989
|
-
const relativeToSkill = file.path.substring(skillDir.path.length + 1);
|
|
3990
|
-
const content = await withSemaphore(
|
|
3991
|
-
semaphore,
|
|
3992
|
-
() => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
|
|
3993
|
-
);
|
|
3994
|
-
skillFiles.push({ relativePath: relativeToSkill, content });
|
|
3995
|
-
}
|
|
3996
|
-
fetchedSkills[skillDir.name] = await writeSkillAndComputeIntegrity({
|
|
3997
|
-
skillName: skillDir.name,
|
|
3998
|
-
files: skillFiles,
|
|
4236
|
+
resolvedSha,
|
|
3999
4237
|
curatedDir,
|
|
4000
4238
|
locked,
|
|
4001
|
-
resolvedSha,
|
|
4002
4239
|
sourceKey,
|
|
4240
|
+
client,
|
|
4241
|
+
semaphore,
|
|
4003
4242
|
logger: logger5
|
|
4004
4243
|
});
|
|
4005
4244
|
logger5.debug(`Fetched skill "${skillDir.name}" from ${sourceKey}`);
|
|
@@ -5908,6 +6147,174 @@ function ensureBody({ body, feature, operation }) {
|
|
|
5908
6147
|
}
|
|
5909
6148
|
return body;
|
|
5910
6149
|
}
|
|
6150
|
+
function requireContent({
|
|
6151
|
+
content,
|
|
6152
|
+
feature
|
|
6153
|
+
}) {
|
|
6154
|
+
if (!content) {
|
|
6155
|
+
throw new Error(`content is required for ${feature} put operation`);
|
|
6156
|
+
}
|
|
6157
|
+
return content;
|
|
6158
|
+
}
|
|
6159
|
+
function executeRule(parsed) {
|
|
6160
|
+
if (parsed.operation === "list") {
|
|
6161
|
+
return ruleTools.listRules.execute();
|
|
6162
|
+
}
|
|
6163
|
+
if (parsed.operation === "get") {
|
|
6164
|
+
return ruleTools.getRule.execute({ relativePathFromCwd: requireTargetPath(parsed) });
|
|
6165
|
+
}
|
|
6166
|
+
if (parsed.operation === "put") {
|
|
6167
|
+
return ruleTools.putRule.execute({
|
|
6168
|
+
relativePathFromCwd: requireTargetPath(parsed),
|
|
6169
|
+
frontmatter: parseFrontmatter({
|
|
6170
|
+
feature: "rule",
|
|
6171
|
+
frontmatter: parsed.frontmatter ?? {}
|
|
6172
|
+
}),
|
|
6173
|
+
body: ensureBody(parsed)
|
|
6174
|
+
});
|
|
6175
|
+
}
|
|
6176
|
+
return ruleTools.deleteRule.execute({ relativePathFromCwd: requireTargetPath(parsed) });
|
|
6177
|
+
}
|
|
6178
|
+
function executeCommand(parsed) {
|
|
6179
|
+
if (parsed.operation === "list") {
|
|
6180
|
+
return commandTools.listCommands.execute();
|
|
6181
|
+
}
|
|
6182
|
+
if (parsed.operation === "get") {
|
|
6183
|
+
return commandTools.getCommand.execute({
|
|
6184
|
+
relativePathFromCwd: requireTargetPath(parsed)
|
|
6185
|
+
});
|
|
6186
|
+
}
|
|
6187
|
+
if (parsed.operation === "put") {
|
|
6188
|
+
return commandTools.putCommand.execute({
|
|
6189
|
+
relativePathFromCwd: requireTargetPath(parsed),
|
|
6190
|
+
frontmatter: parseFrontmatter({
|
|
6191
|
+
feature: "command",
|
|
6192
|
+
frontmatter: parsed.frontmatter ?? {}
|
|
6193
|
+
}),
|
|
6194
|
+
body: ensureBody(parsed)
|
|
6195
|
+
});
|
|
6196
|
+
}
|
|
6197
|
+
return commandTools.deleteCommand.execute({
|
|
6198
|
+
relativePathFromCwd: requireTargetPath(parsed)
|
|
6199
|
+
});
|
|
6200
|
+
}
|
|
6201
|
+
function executeSubagent(parsed) {
|
|
6202
|
+
if (parsed.operation === "list") {
|
|
6203
|
+
return subagentTools.listSubagents.execute();
|
|
6204
|
+
}
|
|
6205
|
+
if (parsed.operation === "get") {
|
|
6206
|
+
return subagentTools.getSubagent.execute({
|
|
6207
|
+
relativePathFromCwd: requireTargetPath(parsed)
|
|
6208
|
+
});
|
|
6209
|
+
}
|
|
6210
|
+
if (parsed.operation === "put") {
|
|
6211
|
+
return subagentTools.putSubagent.execute({
|
|
6212
|
+
relativePathFromCwd: requireTargetPath(parsed),
|
|
6213
|
+
frontmatter: parseFrontmatter({
|
|
6214
|
+
feature: "subagent",
|
|
6215
|
+
frontmatter: parsed.frontmatter ?? {}
|
|
6216
|
+
}),
|
|
6217
|
+
body: ensureBody(parsed)
|
|
6218
|
+
});
|
|
6219
|
+
}
|
|
6220
|
+
return subagentTools.deleteSubagent.execute({
|
|
6221
|
+
relativePathFromCwd: requireTargetPath(parsed)
|
|
6222
|
+
});
|
|
6223
|
+
}
|
|
6224
|
+
function executeSkill(parsed) {
|
|
6225
|
+
if (parsed.operation === "list") {
|
|
6226
|
+
return skillTools.listSkills.execute();
|
|
6227
|
+
}
|
|
6228
|
+
if (parsed.operation === "get") {
|
|
6229
|
+
return skillTools.getSkill.execute({ relativeDirPathFromCwd: requireTargetPath(parsed) });
|
|
6230
|
+
}
|
|
6231
|
+
if (parsed.operation === "put") {
|
|
6232
|
+
return skillTools.putSkill.execute({
|
|
6233
|
+
relativeDirPathFromCwd: requireTargetPath(parsed),
|
|
6234
|
+
frontmatter: parseFrontmatter({
|
|
6235
|
+
feature: "skill",
|
|
6236
|
+
frontmatter: parsed.frontmatter ?? {}
|
|
6237
|
+
}),
|
|
6238
|
+
body: ensureBody(parsed),
|
|
6239
|
+
otherFiles: parsed.otherFiles ?? []
|
|
6240
|
+
});
|
|
6241
|
+
}
|
|
6242
|
+
return skillTools.deleteSkill.execute({
|
|
6243
|
+
relativeDirPathFromCwd: requireTargetPath(parsed)
|
|
6244
|
+
});
|
|
6245
|
+
}
|
|
6246
|
+
function executeIgnore(parsed) {
|
|
6247
|
+
if (parsed.operation === "get") {
|
|
6248
|
+
return ignoreTools.getIgnoreFile.execute();
|
|
6249
|
+
}
|
|
6250
|
+
if (parsed.operation === "put") {
|
|
6251
|
+
return ignoreTools.putIgnoreFile.execute({
|
|
6252
|
+
content: requireContent({ content: parsed.content, feature: "ignore" })
|
|
6253
|
+
});
|
|
6254
|
+
}
|
|
6255
|
+
return ignoreTools.deleteIgnoreFile.execute();
|
|
6256
|
+
}
|
|
6257
|
+
function executeMcp(parsed) {
|
|
6258
|
+
if (parsed.operation === "get") {
|
|
6259
|
+
return mcpTools.getMcpFile.execute();
|
|
6260
|
+
}
|
|
6261
|
+
if (parsed.operation === "put") {
|
|
6262
|
+
return mcpTools.putMcpFile.execute({
|
|
6263
|
+
content: requireContent({ content: parsed.content, feature: "mcp" })
|
|
6264
|
+
});
|
|
6265
|
+
}
|
|
6266
|
+
return mcpTools.deleteMcpFile.execute();
|
|
6267
|
+
}
|
|
6268
|
+
function executePermissions(parsed) {
|
|
6269
|
+
if (parsed.operation === "get") {
|
|
6270
|
+
return permissionsTools.getPermissionsFile.execute();
|
|
6271
|
+
}
|
|
6272
|
+
if (parsed.operation === "put") {
|
|
6273
|
+
return permissionsTools.putPermissionsFile.execute({
|
|
6274
|
+
content: requireContent({ content: parsed.content, feature: "permissions" })
|
|
6275
|
+
});
|
|
6276
|
+
}
|
|
6277
|
+
return permissionsTools.deletePermissionsFile.execute();
|
|
6278
|
+
}
|
|
6279
|
+
function executeHooks(parsed) {
|
|
6280
|
+
if (parsed.operation === "get") {
|
|
6281
|
+
return hooksTools.getHooksFile.execute();
|
|
6282
|
+
}
|
|
6283
|
+
if (parsed.operation === "put") {
|
|
6284
|
+
return hooksTools.putHooksFile.execute({
|
|
6285
|
+
content: requireContent({ content: parsed.content, feature: "hooks" })
|
|
6286
|
+
});
|
|
6287
|
+
}
|
|
6288
|
+
return hooksTools.deleteHooksFile.execute();
|
|
6289
|
+
}
|
|
6290
|
+
function executeGenerate2(parsed) {
|
|
6291
|
+
return generateTools.executeGenerate.execute(parsed.generateOptions ?? {});
|
|
6292
|
+
}
|
|
6293
|
+
function executeImport2(parsed) {
|
|
6294
|
+
if (!parsed.importOptions) {
|
|
6295
|
+
throw new Error("importOptions is required for import feature");
|
|
6296
|
+
}
|
|
6297
|
+
return importTools.executeImport.execute(parsed.importOptions);
|
|
6298
|
+
}
|
|
6299
|
+
function executeConvert2(parsed) {
|
|
6300
|
+
if (!parsed.convertOptions) {
|
|
6301
|
+
throw new Error("convertOptions is required for convert feature");
|
|
6302
|
+
}
|
|
6303
|
+
return convertTools.executeConvert.execute(parsed.convertOptions);
|
|
6304
|
+
}
|
|
6305
|
+
var featureExecutors = {
|
|
6306
|
+
rule: executeRule,
|
|
6307
|
+
command: executeCommand,
|
|
6308
|
+
subagent: executeSubagent,
|
|
6309
|
+
skill: executeSkill,
|
|
6310
|
+
ignore: executeIgnore,
|
|
6311
|
+
mcp: executeMcp,
|
|
6312
|
+
permissions: executePermissions,
|
|
6313
|
+
hooks: executeHooks,
|
|
6314
|
+
generate: executeGenerate2,
|
|
6315
|
+
import: executeImport2,
|
|
6316
|
+
convert: executeConvert2
|
|
6317
|
+
};
|
|
5911
6318
|
var rulesyncTool = {
|
|
5912
6319
|
name: "rulesyncTool",
|
|
5913
6320
|
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.",
|
|
@@ -5915,161 +6322,11 @@ var rulesyncTool = {
|
|
|
5915
6322
|
execute: async (args) => {
|
|
5916
6323
|
const parsed = rulesyncToolSchema.parse(args);
|
|
5917
6324
|
assertSupported({ feature: parsed.feature, operation: parsed.operation });
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
return ruleTools.listRules.execute();
|
|
5922
|
-
}
|
|
5923
|
-
if (parsed.operation === "get") {
|
|
5924
|
-
return ruleTools.getRule.execute({ relativePathFromCwd: requireTargetPath(parsed) });
|
|
5925
|
-
}
|
|
5926
|
-
if (parsed.operation === "put") {
|
|
5927
|
-
return ruleTools.putRule.execute({
|
|
5928
|
-
relativePathFromCwd: requireTargetPath(parsed),
|
|
5929
|
-
frontmatter: parseFrontmatter({
|
|
5930
|
-
feature: "rule",
|
|
5931
|
-
frontmatter: parsed.frontmatter ?? {}
|
|
5932
|
-
}),
|
|
5933
|
-
body: ensureBody(parsed)
|
|
5934
|
-
});
|
|
5935
|
-
}
|
|
5936
|
-
return ruleTools.deleteRule.execute({ relativePathFromCwd: requireTargetPath(parsed) });
|
|
5937
|
-
}
|
|
5938
|
-
case "command": {
|
|
5939
|
-
if (parsed.operation === "list") {
|
|
5940
|
-
return commandTools.listCommands.execute();
|
|
5941
|
-
}
|
|
5942
|
-
if (parsed.operation === "get") {
|
|
5943
|
-
return commandTools.getCommand.execute({
|
|
5944
|
-
relativePathFromCwd: requireTargetPath(parsed)
|
|
5945
|
-
});
|
|
5946
|
-
}
|
|
5947
|
-
if (parsed.operation === "put") {
|
|
5948
|
-
return commandTools.putCommand.execute({
|
|
5949
|
-
relativePathFromCwd: requireTargetPath(parsed),
|
|
5950
|
-
frontmatter: parseFrontmatter({
|
|
5951
|
-
feature: "command",
|
|
5952
|
-
frontmatter: parsed.frontmatter ?? {}
|
|
5953
|
-
}),
|
|
5954
|
-
body: ensureBody(parsed)
|
|
5955
|
-
});
|
|
5956
|
-
}
|
|
5957
|
-
return commandTools.deleteCommand.execute({
|
|
5958
|
-
relativePathFromCwd: requireTargetPath(parsed)
|
|
5959
|
-
});
|
|
5960
|
-
}
|
|
5961
|
-
case "subagent": {
|
|
5962
|
-
if (parsed.operation === "list") {
|
|
5963
|
-
return subagentTools.listSubagents.execute();
|
|
5964
|
-
}
|
|
5965
|
-
if (parsed.operation === "get") {
|
|
5966
|
-
return subagentTools.getSubagent.execute({
|
|
5967
|
-
relativePathFromCwd: requireTargetPath(parsed)
|
|
5968
|
-
});
|
|
5969
|
-
}
|
|
5970
|
-
if (parsed.operation === "put") {
|
|
5971
|
-
return subagentTools.putSubagent.execute({
|
|
5972
|
-
relativePathFromCwd: requireTargetPath(parsed),
|
|
5973
|
-
frontmatter: parseFrontmatter({
|
|
5974
|
-
feature: "subagent",
|
|
5975
|
-
frontmatter: parsed.frontmatter ?? {}
|
|
5976
|
-
}),
|
|
5977
|
-
body: ensureBody(parsed)
|
|
5978
|
-
});
|
|
5979
|
-
}
|
|
5980
|
-
return subagentTools.deleteSubagent.execute({
|
|
5981
|
-
relativePathFromCwd: requireTargetPath(parsed)
|
|
5982
|
-
});
|
|
5983
|
-
}
|
|
5984
|
-
case "skill": {
|
|
5985
|
-
if (parsed.operation === "list") {
|
|
5986
|
-
return skillTools.listSkills.execute();
|
|
5987
|
-
}
|
|
5988
|
-
if (parsed.operation === "get") {
|
|
5989
|
-
return skillTools.getSkill.execute({ relativeDirPathFromCwd: requireTargetPath(parsed) });
|
|
5990
|
-
}
|
|
5991
|
-
if (parsed.operation === "put") {
|
|
5992
|
-
return skillTools.putSkill.execute({
|
|
5993
|
-
relativeDirPathFromCwd: requireTargetPath(parsed),
|
|
5994
|
-
frontmatter: parseFrontmatter({
|
|
5995
|
-
feature: "skill",
|
|
5996
|
-
frontmatter: parsed.frontmatter ?? {}
|
|
5997
|
-
}),
|
|
5998
|
-
body: ensureBody(parsed),
|
|
5999
|
-
otherFiles: parsed.otherFiles ?? []
|
|
6000
|
-
});
|
|
6001
|
-
}
|
|
6002
|
-
return skillTools.deleteSkill.execute({
|
|
6003
|
-
relativeDirPathFromCwd: requireTargetPath(parsed)
|
|
6004
|
-
});
|
|
6005
|
-
}
|
|
6006
|
-
case "ignore": {
|
|
6007
|
-
if (parsed.operation === "get") {
|
|
6008
|
-
return ignoreTools.getIgnoreFile.execute();
|
|
6009
|
-
}
|
|
6010
|
-
if (parsed.operation === "put") {
|
|
6011
|
-
if (!parsed.content) {
|
|
6012
|
-
throw new Error("content is required for ignore put operation");
|
|
6013
|
-
}
|
|
6014
|
-
return ignoreTools.putIgnoreFile.execute({ content: parsed.content });
|
|
6015
|
-
}
|
|
6016
|
-
return ignoreTools.deleteIgnoreFile.execute();
|
|
6017
|
-
}
|
|
6018
|
-
case "mcp": {
|
|
6019
|
-
if (parsed.operation === "get") {
|
|
6020
|
-
return mcpTools.getMcpFile.execute();
|
|
6021
|
-
}
|
|
6022
|
-
if (parsed.operation === "put") {
|
|
6023
|
-
if (!parsed.content) {
|
|
6024
|
-
throw new Error("content is required for mcp put operation");
|
|
6025
|
-
}
|
|
6026
|
-
return mcpTools.putMcpFile.execute({ content: parsed.content });
|
|
6027
|
-
}
|
|
6028
|
-
return mcpTools.deleteMcpFile.execute();
|
|
6029
|
-
}
|
|
6030
|
-
case "permissions": {
|
|
6031
|
-
if (parsed.operation === "get") {
|
|
6032
|
-
return permissionsTools.getPermissionsFile.execute();
|
|
6033
|
-
}
|
|
6034
|
-
if (parsed.operation === "put") {
|
|
6035
|
-
if (!parsed.content) {
|
|
6036
|
-
throw new Error("content is required for permissions put operation");
|
|
6037
|
-
}
|
|
6038
|
-
return permissionsTools.putPermissionsFile.execute({ content: parsed.content });
|
|
6039
|
-
}
|
|
6040
|
-
return permissionsTools.deletePermissionsFile.execute();
|
|
6041
|
-
}
|
|
6042
|
-
case "hooks": {
|
|
6043
|
-
if (parsed.operation === "get") {
|
|
6044
|
-
return hooksTools.getHooksFile.execute();
|
|
6045
|
-
}
|
|
6046
|
-
if (parsed.operation === "put") {
|
|
6047
|
-
if (!parsed.content) {
|
|
6048
|
-
throw new Error("content is required for hooks put operation");
|
|
6049
|
-
}
|
|
6050
|
-
return hooksTools.putHooksFile.execute({ content: parsed.content });
|
|
6051
|
-
}
|
|
6052
|
-
return hooksTools.deleteHooksFile.execute();
|
|
6053
|
-
}
|
|
6054
|
-
case "generate": {
|
|
6055
|
-
return generateTools.executeGenerate.execute(parsed.generateOptions ?? {});
|
|
6056
|
-
}
|
|
6057
|
-
case "import": {
|
|
6058
|
-
if (!parsed.importOptions) {
|
|
6059
|
-
throw new Error("importOptions is required for import feature");
|
|
6060
|
-
}
|
|
6061
|
-
return importTools.executeImport.execute(parsed.importOptions);
|
|
6062
|
-
}
|
|
6063
|
-
case "convert": {
|
|
6064
|
-
if (!parsed.convertOptions) {
|
|
6065
|
-
throw new Error("convertOptions is required for convert feature");
|
|
6066
|
-
}
|
|
6067
|
-
return convertTools.executeConvert.execute(parsed.convertOptions);
|
|
6068
|
-
}
|
|
6069
|
-
default: {
|
|
6070
|
-
throw new Error(`Unknown feature: ${parsed.feature}`);
|
|
6071
|
-
}
|
|
6325
|
+
const executor = featureExecutors[parsed.feature];
|
|
6326
|
+
if (!executor) {
|
|
6327
|
+
throw new Error(`Unknown feature: ${parsed.feature}`);
|
|
6072
6328
|
}
|
|
6329
|
+
return executor(parsed);
|
|
6073
6330
|
}
|
|
6074
6331
|
};
|
|
6075
6332
|
|
|
@@ -6288,103 +6545,150 @@ function parseSha256Sums(content) {
|
|
|
6288
6545
|
}
|
|
6289
6546
|
return result;
|
|
6290
6547
|
}
|
|
6291
|
-
|
|
6292
|
-
const { force = false, token } = options;
|
|
6293
|
-
const updateCheck = await checkForUpdate(currentVersion, token);
|
|
6294
|
-
if (!updateCheck.hasUpdate && !force) {
|
|
6295
|
-
return `Already at the latest version (${currentVersion})`;
|
|
6296
|
-
}
|
|
6548
|
+
function resolveUpdateAssets(release) {
|
|
6297
6549
|
const assetName = getPlatformAssetName();
|
|
6298
6550
|
if (!assetName) {
|
|
6299
6551
|
throw new Error(
|
|
6300
6552
|
`Unsupported platform: ${os.platform()} ${os.arch()}. Please download manually from ${RELEASES_URL}`
|
|
6301
6553
|
);
|
|
6302
6554
|
}
|
|
6303
|
-
const binaryAsset = findAsset(
|
|
6555
|
+
const binaryAsset = findAsset(release, assetName);
|
|
6304
6556
|
if (!binaryAsset) {
|
|
6305
6557
|
throw new Error(
|
|
6306
6558
|
`Binary for ${assetName} not found in release. Please download manually from ${RELEASES_URL}`
|
|
6307
6559
|
);
|
|
6308
6560
|
}
|
|
6309
|
-
const checksumAsset = findAsset(
|
|
6561
|
+
const checksumAsset = findAsset(release, "SHA256SUMS");
|
|
6310
6562
|
if (!checksumAsset) {
|
|
6311
6563
|
throw new Error(
|
|
6312
6564
|
`SHA256SUMS not found in release. Cannot verify download integrity. Please download manually from ${RELEASES_URL}`
|
|
6313
6565
|
);
|
|
6314
6566
|
}
|
|
6315
|
-
|
|
6316
|
-
|
|
6567
|
+
return { assetName, binaryAsset, checksumAsset };
|
|
6568
|
+
}
|
|
6569
|
+
async function downloadAndVerifyBinary(params) {
|
|
6570
|
+
const { tempDir, assetName, binaryAsset, checksumAsset } = params;
|
|
6571
|
+
const tempBinaryPath = path.join(tempDir, assetName);
|
|
6572
|
+
await downloadFile(binaryAsset.browser_download_url, tempBinaryPath);
|
|
6573
|
+
const checksumsPath = path.join(tempDir, "SHA256SUMS");
|
|
6574
|
+
await downloadFile(checksumAsset.browser_download_url, checksumsPath);
|
|
6575
|
+
const checksumsContent = await fs.promises.readFile(checksumsPath, "utf-8");
|
|
6576
|
+
const checksums = parseSha256Sums(checksumsContent);
|
|
6577
|
+
const expectedChecksum = checksums.get(assetName);
|
|
6578
|
+
if (!expectedChecksum) {
|
|
6579
|
+
throw new Error(
|
|
6580
|
+
`Checksum entry for "${assetName}" not found in SHA256SUMS. Cannot verify download integrity.`
|
|
6581
|
+
);
|
|
6582
|
+
}
|
|
6583
|
+
const actualChecksum = await calculateSha256(tempBinaryPath);
|
|
6584
|
+
if (actualChecksum !== expectedChecksum) {
|
|
6585
|
+
throw new Error(
|
|
6586
|
+
`Checksum verification failed. Expected: ${expectedChecksum}, Got: ${actualChecksum}. The download may be corrupted.`
|
|
6587
|
+
);
|
|
6588
|
+
}
|
|
6589
|
+
return tempBinaryPath;
|
|
6590
|
+
}
|
|
6591
|
+
async function replaceCurrentBinary(params) {
|
|
6592
|
+
const { tempBinaryPath, currentExePath, currentDir } = params;
|
|
6593
|
+
const tempInPlace = path.join(currentDir, `.rulesync-update-${crypto.randomUUID()}`);
|
|
6317
6594
|
try {
|
|
6595
|
+
await fs.promises.copyFile(tempBinaryPath, tempInPlace);
|
|
6318
6596
|
if (os.platform() !== "win32") {
|
|
6319
|
-
await fs.promises.chmod(
|
|
6597
|
+
await fs.promises.chmod(tempInPlace, 493);
|
|
6320
6598
|
}
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
const checksums = parseSha256Sums(checksumsContent);
|
|
6327
|
-
const expectedChecksum = checksums.get(assetName);
|
|
6328
|
-
if (!expectedChecksum) {
|
|
6329
|
-
throw new Error(
|
|
6330
|
-
`Checksum entry for "${assetName}" not found in SHA256SUMS. Cannot verify download integrity.`
|
|
6331
|
-
);
|
|
6599
|
+
await fs.promises.rename(tempInPlace, currentExePath);
|
|
6600
|
+
} catch {
|
|
6601
|
+
try {
|
|
6602
|
+
await fs.promises.unlink(tempInPlace);
|
|
6603
|
+
} catch {
|
|
6332
6604
|
}
|
|
6333
|
-
|
|
6334
|
-
if (
|
|
6335
|
-
|
|
6336
|
-
`Checksum verification failed. Expected: ${expectedChecksum}, Got: ${actualChecksum}. The download may be corrupted.`
|
|
6337
|
-
);
|
|
6605
|
+
await fs.promises.copyFile(tempBinaryPath, currentExePath);
|
|
6606
|
+
if (os.platform() !== "win32") {
|
|
6607
|
+
await fs.promises.chmod(currentExePath, 493);
|
|
6338
6608
|
}
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
throw
|
|
6609
|
+
}
|
|
6610
|
+
}
|
|
6611
|
+
async function installVerifiedBinary(params) {
|
|
6612
|
+
const { tempDir, tempBinaryPath, currentVersion, latestVersion } = params;
|
|
6613
|
+
const currentExePath = await fs.promises.realpath(process.execPath);
|
|
6614
|
+
const currentDir = path.dirname(currentExePath);
|
|
6615
|
+
const backupPath = path.join(tempDir, "rulesync.backup");
|
|
6616
|
+
try {
|
|
6617
|
+
await fs.promises.copyFile(currentExePath, backupPath);
|
|
6618
|
+
} catch (error) {
|
|
6619
|
+
if (isPermissionError(error)) {
|
|
6620
|
+
throw new UpdatePermissionError(
|
|
6621
|
+
`Permission denied: Cannot read ${currentExePath}. Try running with sudo.`
|
|
6622
|
+
);
|
|
6351
6623
|
}
|
|
6624
|
+
throw error;
|
|
6625
|
+
}
|
|
6626
|
+
try {
|
|
6627
|
+
await replaceCurrentBinary({ tempBinaryPath, currentExePath, currentDir });
|
|
6628
|
+
return {
|
|
6629
|
+
message: `Successfully updated from ${currentVersion} to ${latestVersion}`,
|
|
6630
|
+
restoreFailed: false
|
|
6631
|
+
};
|
|
6632
|
+
} catch (error) {
|
|
6352
6633
|
try {
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
await fs.promises.chmod(tempInPlace, 493);
|
|
6358
|
-
}
|
|
6359
|
-
await fs.promises.rename(tempInPlace, currentExePath);
|
|
6360
|
-
} catch {
|
|
6361
|
-
try {
|
|
6362
|
-
await fs.promises.unlink(tempInPlace);
|
|
6363
|
-
} catch {
|
|
6364
|
-
}
|
|
6365
|
-
await fs.promises.copyFile(tempBinaryPath, currentExePath);
|
|
6366
|
-
if (os.platform() !== "win32") {
|
|
6367
|
-
await fs.promises.chmod(currentExePath, 493);
|
|
6368
|
-
}
|
|
6369
|
-
}
|
|
6370
|
-
return `Successfully updated from ${currentVersion} to ${updateCheck.latestVersion}`;
|
|
6371
|
-
} catch (error) {
|
|
6372
|
-
try {
|
|
6373
|
-
await fs.promises.copyFile(backupPath, currentExePath);
|
|
6374
|
-
} catch {
|
|
6375
|
-
restoreFailed = true;
|
|
6376
|
-
throw new Error(
|
|
6634
|
+
await fs.promises.copyFile(backupPath, currentExePath);
|
|
6635
|
+
} catch {
|
|
6636
|
+
throw new RestoreFailedError(
|
|
6637
|
+
new Error(
|
|
6377
6638
|
`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)}`,
|
|
6378
6639
|
{ cause: error }
|
|
6379
|
-
)
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
)
|
|
6385
|
-
|
|
6386
|
-
|
|
6640
|
+
)
|
|
6641
|
+
);
|
|
6642
|
+
}
|
|
6643
|
+
if (isPermissionError(error)) {
|
|
6644
|
+
throw new UpdatePermissionError(
|
|
6645
|
+
`Permission denied: Cannot write to ${path.dirname(currentExePath)}. Try running with sudo.`
|
|
6646
|
+
);
|
|
6647
|
+
}
|
|
6648
|
+
throw error;
|
|
6649
|
+
}
|
|
6650
|
+
}
|
|
6651
|
+
var RestoreFailedError = class extends Error {
|
|
6652
|
+
cause;
|
|
6653
|
+
constructor(cause) {
|
|
6654
|
+
super(cause.message);
|
|
6655
|
+
this.name = "RestoreFailedError";
|
|
6656
|
+
this.cause = cause;
|
|
6657
|
+
}
|
|
6658
|
+
};
|
|
6659
|
+
async function performBinaryUpdate(currentVersion, options = {}) {
|
|
6660
|
+
const { force = false, token } = options;
|
|
6661
|
+
const updateCheck = await checkForUpdate(currentVersion, token);
|
|
6662
|
+
if (!updateCheck.hasUpdate && !force) {
|
|
6663
|
+
return `Already at the latest version (${currentVersion})`;
|
|
6664
|
+
}
|
|
6665
|
+
const { assetName, binaryAsset, checksumAsset } = resolveUpdateAssets(updateCheck.release);
|
|
6666
|
+
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "rulesync-update-"));
|
|
6667
|
+
let restoreFailed = false;
|
|
6668
|
+
try {
|
|
6669
|
+
if (os.platform() !== "win32") {
|
|
6670
|
+
await fs.promises.chmod(tempDir, 448);
|
|
6671
|
+
}
|
|
6672
|
+
const tempBinaryPath = await downloadAndVerifyBinary({
|
|
6673
|
+
tempDir,
|
|
6674
|
+
assetName,
|
|
6675
|
+
binaryAsset,
|
|
6676
|
+
checksumAsset
|
|
6677
|
+
});
|
|
6678
|
+
const installed = await installVerifiedBinary({
|
|
6679
|
+
tempDir,
|
|
6680
|
+
tempBinaryPath,
|
|
6681
|
+
currentVersion,
|
|
6682
|
+
latestVersion: updateCheck.latestVersion
|
|
6683
|
+
});
|
|
6684
|
+
restoreFailed = installed.restoreFailed;
|
|
6685
|
+
return installed.message;
|
|
6686
|
+
} catch (error) {
|
|
6687
|
+
if (error instanceof RestoreFailedError) {
|
|
6688
|
+
restoreFailed = true;
|
|
6689
|
+
throw error.cause;
|
|
6387
6690
|
}
|
|
6691
|
+
throw error;
|
|
6388
6692
|
} finally {
|
|
6389
6693
|
if (!restoreFailed) {
|
|
6390
6694
|
try {
|
|
@@ -6515,7 +6819,7 @@ function wrapCommand({
|
|
|
6515
6819
|
}
|
|
6516
6820
|
|
|
6517
6821
|
// src/cli/index.ts
|
|
6518
|
-
var getVersion = () => "
|
|
6822
|
+
var getVersion = () => "9.0.0";
|
|
6519
6823
|
function wrapCommand2(name, errorCode, handler) {
|
|
6520
6824
|
return wrapCommand({ name, errorCode, handler, getVersion });
|
|
6521
6825
|
}
|
|
@@ -6629,10 +6933,6 @@ var main = async () => {
|
|
|
6629
6933
|
"-o, --output-roots <paths>",
|
|
6630
6934
|
"Output root directories to generate files into (comma-separated for multiple paths)",
|
|
6631
6935
|
parseCommaSeparatedList
|
|
6632
|
-
).option(
|
|
6633
|
-
"-b, --base-dir <paths>",
|
|
6634
|
-
"[Deprecated] Use --output-roots instead. Output root directories (comma-separated for multiple paths)",
|
|
6635
|
-
parseCommaSeparatedList
|
|
6636
6936
|
).option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").option("-c, --config <path>", "Path to configuration file").option("-g, --global", "Generate for global(user scope) configuration files").option(
|
|
6637
6937
|
"--simulate-commands",
|
|
6638
6938
|
"Generate simulated commands. This feature is only available for copilot, cursor and codexcli."
|