rulesync 7.15.2 → 7.17.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 +1 -1
- package/dist/{chunk-WY325EI7.js → chunk-ZQUEAGJI.js} +164 -60
- package/dist/cli/index.cjs +331 -194
- package/dist/cli/index.js +177 -138
- package/dist/index.cjs +160 -61
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -14,12 +14,14 @@ import {
|
|
|
14
14
|
RULESYNC_AIIGNORE_RELATIVE_FILE_PATH,
|
|
15
15
|
RULESYNC_COMMANDS_RELATIVE_DIR_PATH,
|
|
16
16
|
RULESYNC_CONFIG_RELATIVE_FILE_PATH,
|
|
17
|
+
RULESYNC_CONFIG_SCHEMA_URL,
|
|
17
18
|
RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH,
|
|
18
19
|
RULESYNC_HOOKS_FILE_NAME,
|
|
19
20
|
RULESYNC_HOOKS_RELATIVE_FILE_PATH,
|
|
20
21
|
RULESYNC_IGNORE_RELATIVE_FILE_PATH,
|
|
21
22
|
RULESYNC_MCP_FILE_NAME,
|
|
22
23
|
RULESYNC_MCP_RELATIVE_FILE_PATH,
|
|
24
|
+
RULESYNC_MCP_SCHEMA_URL,
|
|
23
25
|
RULESYNC_OVERVIEW_FILE_NAME,
|
|
24
26
|
RULESYNC_RELATIVE_DIR_PATH,
|
|
25
27
|
RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
@@ -47,6 +49,7 @@ import {
|
|
|
47
49
|
directoryExists,
|
|
48
50
|
ensureDir,
|
|
49
51
|
fileExists,
|
|
52
|
+
findControlCharacter,
|
|
50
53
|
findFilesByGlobs,
|
|
51
54
|
formatError,
|
|
52
55
|
generate,
|
|
@@ -62,7 +65,7 @@ import {
|
|
|
62
65
|
removeTempDirectory,
|
|
63
66
|
stringifyFrontmatter,
|
|
64
67
|
writeFileContent
|
|
65
|
-
} from "../chunk-
|
|
68
|
+
} from "../chunk-ZQUEAGJI.js";
|
|
66
69
|
|
|
67
70
|
// src/cli/index.ts
|
|
68
71
|
import { Command } from "commander";
|
|
@@ -1383,6 +1386,7 @@ async function createConfigFile() {
|
|
|
1383
1386
|
path2,
|
|
1384
1387
|
JSON.stringify(
|
|
1385
1388
|
{
|
|
1389
|
+
$schema: RULESYNC_CONFIG_SCHEMA_URL,
|
|
1386
1390
|
targets: ["copilot", "cursor", "claudecode", "codexcli"],
|
|
1387
1391
|
features: ["rules", "ignore", "mcp", "commands", "subagents", "skills", "hooks"],
|
|
1388
1392
|
baseDirs: ["."],
|
|
@@ -1440,6 +1444,7 @@ globs: ["**/*"]
|
|
|
1440
1444
|
const sampleMcpFile = {
|
|
1441
1445
|
filename: "mcp.json",
|
|
1442
1446
|
content: `{
|
|
1447
|
+
"$schema": "${RULESYNC_MCP_SCHEMA_URL}",
|
|
1443
1448
|
"mcpServers": {
|
|
1444
1449
|
"serena": {
|
|
1445
1450
|
"type": "stdio",
|
|
@@ -1618,21 +1623,12 @@ import { Semaphore as Semaphore2 } from "es-toolkit/promise";
|
|
|
1618
1623
|
|
|
1619
1624
|
// src/lib/git-client.ts
|
|
1620
1625
|
import { execFile } from "child_process";
|
|
1621
|
-
import { join as join4 } from "path";
|
|
1626
|
+
import { isAbsolute, join as join4, relative } from "path";
|
|
1622
1627
|
import { promisify } from "util";
|
|
1623
1628
|
var execFileAsync = promisify(execFile);
|
|
1624
1629
|
var GIT_TIMEOUT_MS = 6e4;
|
|
1625
|
-
var ALLOWED_URL_SCHEMES = /^(https?:\/\/|ssh:\/\/|git:\/\/|file
|
|
1630
|
+
var ALLOWED_URL_SCHEMES = /^(https?:\/\/|ssh:\/\/|git:\/\/|file:\/\/\/).+$|^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9.-]+:[a-zA-Z0-9_.+/~-]+$/;
|
|
1626
1631
|
var INSECURE_URL_SCHEMES = /^(git:\/\/|http:\/\/)/;
|
|
1627
|
-
function findControlCharacter(value) {
|
|
1628
|
-
for (let i = 0; i < value.length; i++) {
|
|
1629
|
-
const code = value.charCodeAt(i);
|
|
1630
|
-
if (code >= 0 && code <= 31 || code === 127) {
|
|
1631
|
-
return { position: i, hex: `0x${code.toString(16).padStart(2, "0")}` };
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
return null;
|
|
1635
|
-
}
|
|
1636
1632
|
var GitClientError = class extends Error {
|
|
1637
1633
|
constructor(message, cause) {
|
|
1638
1634
|
super(message, { cause });
|
|
@@ -1688,6 +1684,7 @@ async function resolveDefaultRef(url) {
|
|
|
1688
1684
|
const ref = stdout.match(/^ref: refs\/heads\/(.+)\tHEAD$/m)?.[1];
|
|
1689
1685
|
const sha = stdout.match(/^([0-9a-f]{40})\tHEAD$/m)?.[1];
|
|
1690
1686
|
if (!ref || !sha) throw new GitClientError(`Could not parse default branch from: ${url}`);
|
|
1687
|
+
validateRef(ref);
|
|
1691
1688
|
return { ref, sha };
|
|
1692
1689
|
} catch (error) {
|
|
1693
1690
|
if (error instanceof GitClientError) throw error;
|
|
@@ -1714,6 +1711,17 @@ async function fetchSkillFiles(params) {
|
|
|
1714
1711
|
const { url, ref, skillsPath } = params;
|
|
1715
1712
|
validateGitUrl(url);
|
|
1716
1713
|
validateRef(ref);
|
|
1714
|
+
if (skillsPath.split(/[/\\]/).includes("..") || isAbsolute(skillsPath)) {
|
|
1715
|
+
throw new GitClientError(
|
|
1716
|
+
`Invalid skillsPath "${skillsPath}": must be a relative path without ".."`
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
const ctrl = findControlCharacter(skillsPath);
|
|
1720
|
+
if (ctrl) {
|
|
1721
|
+
throw new GitClientError(
|
|
1722
|
+
`skillsPath contains control character ${ctrl.hex} at position ${ctrl.position}`
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1717
1725
|
await checkGitAvailable();
|
|
1718
1726
|
const tmpDir = await createTempDirectory("rulesync-git-");
|
|
1719
1727
|
try {
|
|
@@ -1748,7 +1756,9 @@ async function fetchSkillFiles(params) {
|
|
|
1748
1756
|
}
|
|
1749
1757
|
}
|
|
1750
1758
|
var MAX_WALK_DEPTH = 20;
|
|
1751
|
-
|
|
1759
|
+
var MAX_TOTAL_FILES = 1e4;
|
|
1760
|
+
var MAX_TOTAL_SIZE = 100 * 1024 * 1024;
|
|
1761
|
+
async function walkDirectory(dir, baseDir, depth = 0, ctx = { totalFiles: 0, totalSize: 0 }) {
|
|
1752
1762
|
if (depth > MAX_WALK_DEPTH) {
|
|
1753
1763
|
throw new GitClientError(
|
|
1754
1764
|
`Directory tree exceeds max depth of ${MAX_WALK_DEPTH}: "${dir}". Aborting to prevent resource exhaustion.`
|
|
@@ -1763,7 +1773,7 @@ async function walkDirectory(dir, baseDir, depth = 0) {
|
|
|
1763
1773
|
continue;
|
|
1764
1774
|
}
|
|
1765
1775
|
if (await directoryExists(fullPath)) {
|
|
1766
|
-
results.push(...await walkDirectory(fullPath, baseDir, depth + 1));
|
|
1776
|
+
results.push(...await walkDirectory(fullPath, baseDir, depth + 1, ctx));
|
|
1767
1777
|
} else {
|
|
1768
1778
|
const size = await getFileSize(fullPath);
|
|
1769
1779
|
if (size > MAX_FILE_SIZE) {
|
|
@@ -1772,8 +1782,20 @@ async function walkDirectory(dir, baseDir, depth = 0) {
|
|
|
1772
1782
|
);
|
|
1773
1783
|
continue;
|
|
1774
1784
|
}
|
|
1785
|
+
ctx.totalFiles++;
|
|
1786
|
+
ctx.totalSize += size;
|
|
1787
|
+
if (ctx.totalFiles >= MAX_TOTAL_FILES) {
|
|
1788
|
+
throw new GitClientError(
|
|
1789
|
+
`Repository exceeds max file count of ${MAX_TOTAL_FILES}. Aborting to prevent resource exhaustion.`
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
if (ctx.totalSize >= MAX_TOTAL_SIZE) {
|
|
1793
|
+
throw new GitClientError(
|
|
1794
|
+
`Repository exceeds max total size of ${MAX_TOTAL_SIZE / 1024 / 1024}MB. Aborting to prevent resource exhaustion.`
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1775
1797
|
const content = await readFileContent(fullPath);
|
|
1776
|
-
results.push({ relativePath:
|
|
1798
|
+
results.push({ relativePath: relative(baseDir, fullPath), content, size });
|
|
1777
1799
|
}
|
|
1778
1800
|
}
|
|
1779
1801
|
return results;
|
|
@@ -1782,14 +1804,14 @@ async function walkDirectory(dir, baseDir, depth = 0) {
|
|
|
1782
1804
|
// src/lib/sources-lock.ts
|
|
1783
1805
|
import { createHash } from "crypto";
|
|
1784
1806
|
import { join as join5 } from "path";
|
|
1785
|
-
import { optional, z as z4 } from "zod/mini";
|
|
1807
|
+
import { optional, refine, z as z4 } from "zod/mini";
|
|
1786
1808
|
var LOCKFILE_VERSION = 1;
|
|
1787
1809
|
var LockedSkillSchema = z4.object({
|
|
1788
1810
|
integrity: z4.string()
|
|
1789
1811
|
});
|
|
1790
1812
|
var LockedSourceSchema = z4.object({
|
|
1791
1813
|
requestedRef: optional(z4.string()),
|
|
1792
|
-
resolvedRef: z4.string(),
|
|
1814
|
+
resolvedRef: z4.string().check(refine((v) => /^[0-9a-f]{40}$/.test(v), "resolvedRef must be a 40-character hex SHA")),
|
|
1793
1815
|
resolvedAt: optional(z4.string()),
|
|
1794
1816
|
skills: z4.record(z4.string(), LockedSkillSchema)
|
|
1795
1817
|
});
|
|
@@ -1992,6 +2014,8 @@ async function resolveAndFetchSources(params) {
|
|
|
1992
2014
|
logger.error(`Failed to fetch source "${sourceEntry.source}": ${formatError(error)}`);
|
|
1993
2015
|
if (error instanceof GitHubClientError) {
|
|
1994
2016
|
logGitHubAuthHints(error);
|
|
2017
|
+
} else if (error instanceof GitClientError) {
|
|
2018
|
+
logGitClientHints(error);
|
|
1995
2019
|
}
|
|
1996
2020
|
}
|
|
1997
2021
|
}
|
|
@@ -2012,6 +2036,13 @@ async function resolveAndFetchSources(params) {
|
|
|
2012
2036
|
}
|
|
2013
2037
|
return { fetchedSkillCount: totalSkillCount, sourcesProcessed: sources.length };
|
|
2014
2038
|
}
|
|
2039
|
+
function logGitClientHints(error) {
|
|
2040
|
+
if (error.message.includes("not installed")) {
|
|
2041
|
+
logger.info("Hint: Install git and ensure it is available on your PATH.");
|
|
2042
|
+
} else {
|
|
2043
|
+
logger.info("Hint: Check your git credentials (SSH keys, credential helper, or access token).");
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2015
2046
|
async function checkLockedSkillsExist(curatedDir, skillNames) {
|
|
2016
2047
|
if (skillNames.length === 0) return true;
|
|
2017
2048
|
for (const name of skillNames) {
|
|
@@ -2021,9 +2052,88 @@ async function checkLockedSkillsExist(curatedDir, skillNames) {
|
|
|
2021
2052
|
}
|
|
2022
2053
|
return true;
|
|
2023
2054
|
}
|
|
2055
|
+
async function cleanPreviousCuratedSkills(curatedDir, lockedSkillNames) {
|
|
2056
|
+
const resolvedCuratedDir = resolve(curatedDir);
|
|
2057
|
+
for (const prevSkill of lockedSkillNames) {
|
|
2058
|
+
const prevDir = join6(curatedDir, prevSkill);
|
|
2059
|
+
if (!resolve(prevDir).startsWith(resolvedCuratedDir + sep)) {
|
|
2060
|
+
logger.warn(
|
|
2061
|
+
`Skipping removal of "${prevSkill}": resolved path is outside the curated directory.`
|
|
2062
|
+
);
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
if (await directoryExists(prevDir)) {
|
|
2066
|
+
await removeDirectory(prevDir);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
function shouldSkipSkill(params) {
|
|
2071
|
+
const { skillName, sourceKey, localSkillNames, alreadyFetchedSkillNames } = params;
|
|
2072
|
+
if (skillName.includes("..") || skillName.includes("/") || skillName.includes("\\")) {
|
|
2073
|
+
logger.warn(
|
|
2074
|
+
`Skipping skill with invalid name "${skillName}" from ${sourceKey}: contains path traversal characters.`
|
|
2075
|
+
);
|
|
2076
|
+
return true;
|
|
2077
|
+
}
|
|
2078
|
+
if (localSkillNames.has(skillName)) {
|
|
2079
|
+
logger.debug(
|
|
2080
|
+
`Skipping remote skill "${skillName}" from ${sourceKey}: local skill takes precedence.`
|
|
2081
|
+
);
|
|
2082
|
+
return true;
|
|
2083
|
+
}
|
|
2084
|
+
if (alreadyFetchedSkillNames.has(skillName)) {
|
|
2085
|
+
logger.warn(
|
|
2086
|
+
`Skipping duplicate skill "${skillName}" from ${sourceKey}: already fetched from another source.`
|
|
2087
|
+
);
|
|
2088
|
+
return true;
|
|
2089
|
+
}
|
|
2090
|
+
return false;
|
|
2091
|
+
}
|
|
2092
|
+
async function writeSkillAndComputeIntegrity(params) {
|
|
2093
|
+
const { skillName, files, curatedDir, locked, resolvedSha, sourceKey } = params;
|
|
2094
|
+
const written = [];
|
|
2095
|
+
for (const file of files) {
|
|
2096
|
+
checkPathTraversal({
|
|
2097
|
+
relativePath: file.relativePath,
|
|
2098
|
+
intendedRootDir: join6(curatedDir, skillName)
|
|
2099
|
+
});
|
|
2100
|
+
await writeFileContent(join6(curatedDir, skillName, file.relativePath), file.content);
|
|
2101
|
+
written.push({ path: file.relativePath, content: file.content });
|
|
2102
|
+
}
|
|
2103
|
+
const integrity = computeSkillIntegrity(written);
|
|
2104
|
+
const lockedSkillEntry = locked?.skills[skillName];
|
|
2105
|
+
if (lockedSkillEntry?.integrity && lockedSkillEntry.integrity !== integrity && resolvedSha === locked?.resolvedRef) {
|
|
2106
|
+
logger.warn(
|
|
2107
|
+
`Integrity mismatch for skill "${skillName}" from ${sourceKey}: expected "${lockedSkillEntry.integrity}", got "${integrity}". Content may have been tampered with.`
|
|
2108
|
+
);
|
|
2109
|
+
}
|
|
2110
|
+
return { integrity };
|
|
2111
|
+
}
|
|
2112
|
+
function buildLockUpdate(params) {
|
|
2113
|
+
const { lock, sourceKey, fetchedSkills, locked, requestedRef, resolvedSha } = params;
|
|
2114
|
+
const fetchedNames = Object.keys(fetchedSkills);
|
|
2115
|
+
const mergedSkills = { ...fetchedSkills };
|
|
2116
|
+
if (locked) {
|
|
2117
|
+
for (const [skillName, skillEntry] of Object.entries(locked.skills)) {
|
|
2118
|
+
if (!(skillName in mergedSkills)) {
|
|
2119
|
+
mergedSkills[skillName] = skillEntry;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
const updatedLock = setLockedSource(lock, sourceKey, {
|
|
2124
|
+
requestedRef,
|
|
2125
|
+
resolvedRef: resolvedSha,
|
|
2126
|
+
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2127
|
+
skills: mergedSkills
|
|
2128
|
+
});
|
|
2129
|
+
logger.info(
|
|
2130
|
+
`Fetched ${fetchedNames.length} skill(s) from ${sourceKey}: ${fetchedNames.join(", ") || "(none)"}`
|
|
2131
|
+
);
|
|
2132
|
+
return { updatedLock, fetchedNames };
|
|
2133
|
+
}
|
|
2024
2134
|
async function fetchSource(params) {
|
|
2025
2135
|
const { sourceEntry, client, baseDir, localSkillNames, alreadyFetchedSkillNames, updateSources } = params;
|
|
2026
|
-
|
|
2136
|
+
const { lock } = params;
|
|
2027
2137
|
const parsed = parseSource(sourceEntry.source);
|
|
2028
2138
|
if (parsed.provider === "gitlab") {
|
|
2029
2139
|
logger.warn(`GitLab sources are not yet supported. Skipping "${sourceEntry.source}".`);
|
|
@@ -2076,37 +2186,15 @@ async function fetchSource(params) {
|
|
|
2076
2186
|
const semaphore = new Semaphore2(FETCH_CONCURRENCY_LIMIT);
|
|
2077
2187
|
const fetchedSkills = {};
|
|
2078
2188
|
if (locked) {
|
|
2079
|
-
|
|
2080
|
-
for (const prevSkill of lockedSkillNames) {
|
|
2081
|
-
const prevDir = join6(curatedDir, prevSkill);
|
|
2082
|
-
if (!resolve(prevDir).startsWith(resolvedCuratedDir + sep)) {
|
|
2083
|
-
logger.warn(
|
|
2084
|
-
`Skipping removal of "${prevSkill}": resolved path is outside the curated directory.`
|
|
2085
|
-
);
|
|
2086
|
-
continue;
|
|
2087
|
-
}
|
|
2088
|
-
if (await directoryExists(prevDir)) {
|
|
2089
|
-
await removeDirectory(prevDir);
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2189
|
+
await cleanPreviousCuratedSkills(curatedDir, lockedSkillNames);
|
|
2092
2190
|
}
|
|
2093
2191
|
for (const skillDir of filteredDirs) {
|
|
2094
|
-
if (
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
}
|
|
2100
|
-
if (localSkillNames.has(skillDir.name)) {
|
|
2101
|
-
logger.debug(
|
|
2102
|
-
`Skipping remote skill "${skillDir.name}" from ${sourceKey}: local skill takes precedence.`
|
|
2103
|
-
);
|
|
2104
|
-
continue;
|
|
2105
|
-
}
|
|
2106
|
-
if (alreadyFetchedSkillNames.has(skillDir.name)) {
|
|
2107
|
-
logger.warn(
|
|
2108
|
-
`Skipping duplicate skill "${skillDir.name}" from ${sourceKey}: already fetched from another source.`
|
|
2109
|
-
);
|
|
2192
|
+
if (shouldSkipSkill({
|
|
2193
|
+
skillName: skillDir.name,
|
|
2194
|
+
sourceKey,
|
|
2195
|
+
localSkillNames,
|
|
2196
|
+
alreadyFetchedSkillNames
|
|
2197
|
+
})) {
|
|
2110
2198
|
continue;
|
|
2111
2199
|
}
|
|
2112
2200
|
const allFiles = await listDirectoryRecursive({
|
|
@@ -2129,55 +2217,39 @@ async function fetchSource(params) {
|
|
|
2129
2217
|
const skillFiles = [];
|
|
2130
2218
|
for (const file of files) {
|
|
2131
2219
|
const relativeToSkill = file.path.substring(skillDir.path.length + 1);
|
|
2132
|
-
const localFilePath = join6(curatedDir, skillDir.name, relativeToSkill);
|
|
2133
|
-
checkPathTraversal({
|
|
2134
|
-
relativePath: relativeToSkill,
|
|
2135
|
-
intendedRootDir: join6(curatedDir, skillDir.name)
|
|
2136
|
-
});
|
|
2137
2220
|
const content = await withSemaphore(
|
|
2138
2221
|
semaphore,
|
|
2139
2222
|
() => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
|
|
2140
2223
|
);
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
}
|
|
2151
|
-
fetchedSkills[skillDir.name] = { integrity };
|
|
2224
|
+
skillFiles.push({ relativePath: relativeToSkill, content });
|
|
2225
|
+
}
|
|
2226
|
+
fetchedSkills[skillDir.name] = await writeSkillAndComputeIntegrity({
|
|
2227
|
+
skillName: skillDir.name,
|
|
2228
|
+
files: skillFiles,
|
|
2229
|
+
curatedDir,
|
|
2230
|
+
locked,
|
|
2231
|
+
resolvedSha,
|
|
2232
|
+
sourceKey
|
|
2233
|
+
});
|
|
2152
2234
|
logger.debug(`Fetched skill "${skillDir.name}" from ${sourceKey}`);
|
|
2153
2235
|
}
|
|
2154
|
-
const
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
mergedSkills[skillName] = skillEntry;
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
}
|
|
2163
|
-
lock = setLockedSource(lock, sourceKey, {
|
|
2236
|
+
const result = buildLockUpdate({
|
|
2237
|
+
lock,
|
|
2238
|
+
sourceKey,
|
|
2239
|
+
fetchedSkills,
|
|
2240
|
+
locked,
|
|
2164
2241
|
requestedRef,
|
|
2165
|
-
|
|
2166
|
-
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2167
|
-
skills: mergedSkills
|
|
2242
|
+
resolvedSha
|
|
2168
2243
|
});
|
|
2169
|
-
logger.info(
|
|
2170
|
-
`Fetched ${fetchedNames.length} skill(s) from ${sourceKey}: ${fetchedNames.join(", ") || "(none)"}`
|
|
2171
|
-
);
|
|
2172
2244
|
return {
|
|
2173
|
-
skillCount: fetchedNames.length,
|
|
2174
|
-
fetchedSkillNames: fetchedNames,
|
|
2175
|
-
updatedLock:
|
|
2245
|
+
skillCount: result.fetchedNames.length,
|
|
2246
|
+
fetchedSkillNames: result.fetchedNames,
|
|
2247
|
+
updatedLock: result.updatedLock
|
|
2176
2248
|
};
|
|
2177
2249
|
}
|
|
2178
2250
|
async function fetchSourceViaGit(params) {
|
|
2179
2251
|
const { sourceEntry, baseDir, localSkillNames, alreadyFetchedSkillNames, updateSources, frozen } = params;
|
|
2180
|
-
|
|
2252
|
+
const { lock } = params;
|
|
2181
2253
|
const url = sourceEntry.source;
|
|
2182
2254
|
const locked = getLockedSource(lock, url);
|
|
2183
2255
|
const lockedSkillNames = locked ? getLockedSkillNames(locked) : [];
|
|
@@ -2233,68 +2305,35 @@ async function fetchSourceViaGit(params) {
|
|
|
2233
2305
|
const allNames = [...skillFileMap.keys()];
|
|
2234
2306
|
const filteredNames = isWildcard ? allNames : allNames.filter((n) => skillFilter.includes(n));
|
|
2235
2307
|
if (locked) {
|
|
2236
|
-
|
|
2237
|
-
for (const prev of lockedSkillNames) {
|
|
2238
|
-
const dir = join6(curatedDir, prev);
|
|
2239
|
-
if (resolve(dir).startsWith(base + sep) && await directoryExists(dir)) {
|
|
2240
|
-
await removeDirectory(dir);
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2308
|
+
await cleanPreviousCuratedSkills(curatedDir, lockedSkillNames);
|
|
2243
2309
|
}
|
|
2244
2310
|
const fetchedSkills = {};
|
|
2245
2311
|
for (const skillName of filteredNames) {
|
|
2246
|
-
if (
|
|
2247
|
-
logger.warn(
|
|
2248
|
-
`Skipping skill with invalid name "${skillName}" from ${url}: contains path traversal characters.`
|
|
2249
|
-
);
|
|
2250
|
-
continue;
|
|
2251
|
-
}
|
|
2252
|
-
if (localSkillNames.has(skillName)) {
|
|
2253
|
-
logger.debug(
|
|
2254
|
-
`Skipping remote skill "${skillName}" from ${url}: local skill takes precedence.`
|
|
2255
|
-
);
|
|
2256
|
-
continue;
|
|
2257
|
-
}
|
|
2258
|
-
if (alreadyFetchedSkillNames.has(skillName)) {
|
|
2259
|
-
logger.warn(
|
|
2260
|
-
`Skipping duplicate skill "${skillName}" from ${url}: already fetched from another source.`
|
|
2261
|
-
);
|
|
2312
|
+
if (shouldSkipSkill({ skillName, sourceKey: url, localSkillNames, alreadyFetchedSkillNames })) {
|
|
2262
2313
|
continue;
|
|
2263
2314
|
}
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
written.push({ path: file.relativePath, content: file.content });
|
|
2273
|
-
}
|
|
2274
|
-
const integrity = computeSkillIntegrity(written);
|
|
2275
|
-
const lockedSkillEntry = locked?.skills[skillName];
|
|
2276
|
-
if (lockedSkillEntry?.integrity && lockedSkillEntry.integrity !== integrity && resolvedSha === locked?.resolvedRef) {
|
|
2277
|
-
logger.warn(`Integrity mismatch for skill "${skillName}" from ${url}.`);
|
|
2278
|
-
}
|
|
2279
|
-
fetchedSkills[skillName] = { integrity };
|
|
2280
|
-
}
|
|
2281
|
-
const fetchedNames = Object.keys(fetchedSkills);
|
|
2282
|
-
const mergedSkills = { ...fetchedSkills };
|
|
2283
|
-
if (locked) {
|
|
2284
|
-
for (const [k, v] of Object.entries(locked.skills)) {
|
|
2285
|
-
if (!(k in mergedSkills)) mergedSkills[k] = v;
|
|
2286
|
-
}
|
|
2315
|
+
fetchedSkills[skillName] = await writeSkillAndComputeIntegrity({
|
|
2316
|
+
skillName,
|
|
2317
|
+
files: skillFileMap.get(skillName) ?? [],
|
|
2318
|
+
curatedDir,
|
|
2319
|
+
locked,
|
|
2320
|
+
resolvedSha,
|
|
2321
|
+
sourceKey: url
|
|
2322
|
+
});
|
|
2287
2323
|
}
|
|
2288
|
-
|
|
2324
|
+
const result = buildLockUpdate({
|
|
2325
|
+
lock,
|
|
2326
|
+
sourceKey: url,
|
|
2327
|
+
fetchedSkills,
|
|
2328
|
+
locked,
|
|
2289
2329
|
requestedRef,
|
|
2290
|
-
|
|
2291
|
-
resolvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2292
|
-
skills: mergedSkills
|
|
2330
|
+
resolvedSha
|
|
2293
2331
|
});
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2332
|
+
return {
|
|
2333
|
+
skillCount: result.fetchedNames.length,
|
|
2334
|
+
fetchedSkillNames: result.fetchedNames,
|
|
2335
|
+
updatedLock: result.updatedLock
|
|
2336
|
+
};
|
|
2298
2337
|
}
|
|
2299
2338
|
|
|
2300
2339
|
// src/cli/commands/install.ts
|
|
@@ -4110,7 +4149,7 @@ async function updateCommand(currentVersion, options) {
|
|
|
4110
4149
|
}
|
|
4111
4150
|
|
|
4112
4151
|
// src/cli/index.ts
|
|
4113
|
-
var getVersion = () => "7.
|
|
4152
|
+
var getVersion = () => "7.17.0";
|
|
4114
4153
|
var main = async () => {
|
|
4115
4154
|
const program = new Command();
|
|
4116
4155
|
const version = getVersion();
|