rulesync 8.6.0 → 8.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ILMHM7BF.js → chunk-QGQQJNZD.js} +460 -464
- package/dist/cli/index.cjs +2474 -864
- package/dist/cli/index.js +1831 -211
- package/dist/index.cjs +457 -464
- package/dist/index.js +1 -1
- package/package.json +3 -2
package/dist/cli/index.js
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
RULESYNC_MCP_SCHEMA_URL,
|
|
32
32
|
RULESYNC_OVERVIEW_FILE_NAME,
|
|
33
33
|
RULESYNC_PERMISSIONS_FILE_NAME,
|
|
34
|
+
RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH,
|
|
34
35
|
RULESYNC_RELATIVE_DIR_PATH,
|
|
35
36
|
RULESYNC_RULES_RELATIVE_DIR_PATH,
|
|
36
37
|
RULESYNC_SKILLS_RELATIVE_DIR_PATH,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
RulesyncHooks,
|
|
43
44
|
RulesyncIgnore,
|
|
44
45
|
RulesyncMcp,
|
|
46
|
+
RulesyncPermissions,
|
|
45
47
|
RulesyncRule,
|
|
46
48
|
RulesyncRuleFrontmatterSchema,
|
|
47
49
|
RulesyncSkill,
|
|
@@ -62,6 +64,7 @@ import {
|
|
|
62
64
|
formatError,
|
|
63
65
|
generate,
|
|
64
66
|
getFileSize,
|
|
67
|
+
getHomeDirectory,
|
|
65
68
|
getLocalSkillDirNames,
|
|
66
69
|
importFromTool,
|
|
67
70
|
isFeatureValueEnabled,
|
|
@@ -74,7 +77,7 @@ import {
|
|
|
74
77
|
stringifyFrontmatter,
|
|
75
78
|
toPosixPath,
|
|
76
79
|
writeFileContent
|
|
77
|
-
} from "../chunk-
|
|
80
|
+
} from "../chunk-QGQQJNZD.js";
|
|
78
81
|
|
|
79
82
|
// src/cli/index.ts
|
|
80
83
|
import { Command } from "commander";
|
|
@@ -1948,13 +1951,1171 @@ async function initCommand(logger5) {
|
|
|
1948
1951
|
logger5.info("2. Run 'rulesync generate' to create configuration files");
|
|
1949
1952
|
}
|
|
1950
1953
|
|
|
1951
|
-
// src/lib/
|
|
1952
|
-
import {
|
|
1954
|
+
// src/lib/apm/apm-install.ts
|
|
1955
|
+
import { createHash } from "crypto";
|
|
1956
|
+
import { join as join6, posix as posix2 } from "path";
|
|
1953
1957
|
import { Semaphore as Semaphore2 } from "es-toolkit/promise";
|
|
1954
1958
|
|
|
1959
|
+
// src/lib/apm/apm-lock.ts
|
|
1960
|
+
import { join as join4 } from "path";
|
|
1961
|
+
import { dump, load } from "js-yaml";
|
|
1962
|
+
import { nonnegative, optional, refine, z as z4 } from "zod/mini";
|
|
1963
|
+
var APM_LOCKFILE_FILE_NAME = "rulesync-apm.lock.yaml";
|
|
1964
|
+
var APM_LOCKFILE_VERSION = "1";
|
|
1965
|
+
var RULESYNC_CONTENT_HASH_REGEX = /^sha256:[0-9a-f]{64}$/;
|
|
1966
|
+
var ApmLockDependencySchema = z4.looseObject({
|
|
1967
|
+
repo_url: z4.string(),
|
|
1968
|
+
resolved_commit: optional(
|
|
1969
|
+
z4.string().check(refine((v) => /^[0-9a-f]{40}$/.test(v), "resolved_commit must be a 40-char hex SHA"))
|
|
1970
|
+
),
|
|
1971
|
+
resolved_ref: optional(z4.string()),
|
|
1972
|
+
version: optional(z4.string()),
|
|
1973
|
+
depth: z4.int().check(nonnegative()),
|
|
1974
|
+
resolved_by: optional(z4.string()),
|
|
1975
|
+
package_type: z4.string(),
|
|
1976
|
+
// Intentionally loose: the upstream `apm` CLI may write content_hash values
|
|
1977
|
+
// that do not match the strict rulesync format. We accept any string on read
|
|
1978
|
+
// so that a lockfile produced by `apm` round-trips through rulesync without
|
|
1979
|
+
// throwing. Rulesync itself always writes values matching
|
|
1980
|
+
// `RULESYNC_CONTENT_HASH_REGEX`, and `--frozen` integrity checks only
|
|
1981
|
+
// enforce the comparison when the recorded hash matches that shape.
|
|
1982
|
+
content_hash: optional(z4.string()),
|
|
1983
|
+
is_dev: optional(z4.boolean()),
|
|
1984
|
+
deployed_files: z4.array(z4.string()),
|
|
1985
|
+
source: optional(z4.string()),
|
|
1986
|
+
local_path: optional(z4.string()),
|
|
1987
|
+
virtual_path: optional(z4.string()),
|
|
1988
|
+
is_virtual: optional(z4.boolean())
|
|
1989
|
+
});
|
|
1990
|
+
var ApmLockSchema = z4.looseObject({
|
|
1991
|
+
lockfile_version: z4.literal("1"),
|
|
1992
|
+
generated_at: z4.string(),
|
|
1993
|
+
apm_version: z4.string(),
|
|
1994
|
+
dependencies: z4.array(ApmLockDependencySchema),
|
|
1995
|
+
mcp_servers: optional(z4.array(z4.string()))
|
|
1996
|
+
});
|
|
1997
|
+
function getApmLockPath(baseDir) {
|
|
1998
|
+
return join4(baseDir, APM_LOCKFILE_FILE_NAME);
|
|
1999
|
+
}
|
|
2000
|
+
function createEmptyApmLock(params) {
|
|
2001
|
+
const base = params.existingLock ? { ...params.existingLock } : {};
|
|
2002
|
+
return {
|
|
2003
|
+
...base,
|
|
2004
|
+
lockfile_version: APM_LOCKFILE_VERSION,
|
|
2005
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2006
|
+
apm_version: params.apmVersion,
|
|
2007
|
+
dependencies: []
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
function parseApmLock(content) {
|
|
2011
|
+
if (!content.trim()) {
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
let loaded;
|
|
2015
|
+
try {
|
|
2016
|
+
loaded = load(content);
|
|
2017
|
+
} catch {
|
|
2018
|
+
return null;
|
|
2019
|
+
}
|
|
2020
|
+
if (!loaded || typeof loaded !== "object") {
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
const parsed = ApmLockSchema.safeParse(loaded);
|
|
2024
|
+
if (!parsed.success) {
|
|
2025
|
+
const issues = parsed.error.issues.map((issue) => ` - ${issue.path.join(".") || "<root>"}: ${issue.message}`).join("\n");
|
|
2026
|
+
throw new Error(`Invalid ${APM_LOCKFILE_FILE_NAME}:
|
|
2027
|
+
${issues}`);
|
|
2028
|
+
}
|
|
2029
|
+
return parsed.data;
|
|
2030
|
+
}
|
|
2031
|
+
async function readApmLock(baseDir) {
|
|
2032
|
+
const path2 = getApmLockPath(baseDir);
|
|
2033
|
+
if (!await fileExists(path2)) {
|
|
2034
|
+
return null;
|
|
2035
|
+
}
|
|
2036
|
+
const content = await readFileContent(path2);
|
|
2037
|
+
return parseApmLock(content);
|
|
2038
|
+
}
|
|
2039
|
+
async function writeApmLock(params) {
|
|
2040
|
+
const path2 = getApmLockPath(params.baseDir);
|
|
2041
|
+
const content = serializeApmLock(params.lock);
|
|
2042
|
+
await writeFileContent(path2, content);
|
|
2043
|
+
}
|
|
2044
|
+
function serializeApmLock(lock) {
|
|
2045
|
+
return dump(lock, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2046
|
+
}
|
|
2047
|
+
function findApmLockDependency(lock, repoUrl) {
|
|
2048
|
+
const target = repoUrl.toLowerCase();
|
|
2049
|
+
return lock.dependencies.find((d) => d.repo_url.toLowerCase() === target);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// src/lib/apm/apm-manifest.ts
|
|
2053
|
+
import { join as join5 } from "path";
|
|
2054
|
+
import { dump as dump2, load as load2 } from "js-yaml";
|
|
2055
|
+
import { optional as optional2, z as z5 } from "zod/mini";
|
|
2056
|
+
var APM_MANIFEST_FILE_NAME = "apm.yml";
|
|
2057
|
+
var ApmObjectDependencySchema = z5.looseObject({
|
|
2058
|
+
git: optional2(z5.string()),
|
|
2059
|
+
source: optional2(z5.string()),
|
|
2060
|
+
path: optional2(z5.string()),
|
|
2061
|
+
ref: optional2(z5.string()),
|
|
2062
|
+
alias: optional2(z5.string())
|
|
2063
|
+
});
|
|
2064
|
+
var ApmDependencyInputSchema = z5.union([z5.string(), ApmObjectDependencySchema]);
|
|
2065
|
+
var ApmManifestSchema = z5.looseObject({
|
|
2066
|
+
name: optional2(z5.string()),
|
|
2067
|
+
version: optional2(z5.string()),
|
|
2068
|
+
dependencies: optional2(
|
|
2069
|
+
z5.looseObject({
|
|
2070
|
+
apm: optional2(z5.array(ApmDependencyInputSchema))
|
|
2071
|
+
})
|
|
2072
|
+
)
|
|
2073
|
+
});
|
|
2074
|
+
function getApmManifestPath(baseDir) {
|
|
2075
|
+
return join5(baseDir, APM_MANIFEST_FILE_NAME);
|
|
2076
|
+
}
|
|
2077
|
+
async function apmManifestExists(baseDir) {
|
|
2078
|
+
return fileExists(getApmManifestPath(baseDir));
|
|
2079
|
+
}
|
|
2080
|
+
function parseApmManifest(content) {
|
|
2081
|
+
const loaded = load2(content);
|
|
2082
|
+
if (loaded === void 0 || loaded === null) {
|
|
2083
|
+
return { dependencies: [] };
|
|
2084
|
+
}
|
|
2085
|
+
const parsed = ApmManifestSchema.safeParse(loaded);
|
|
2086
|
+
if (!parsed.success) {
|
|
2087
|
+
throw new Error(`Invalid apm.yml: ${parsed.error.message}`);
|
|
2088
|
+
}
|
|
2089
|
+
const raw = parsed.data;
|
|
2090
|
+
const rawDeps = raw.dependencies?.apm ?? [];
|
|
2091
|
+
const dependencies = rawDeps.map(
|
|
2092
|
+
(entry, index) => normalizeDependency(entry, index)
|
|
2093
|
+
);
|
|
2094
|
+
return {
|
|
2095
|
+
name: raw.name,
|
|
2096
|
+
version: raw.version,
|
|
2097
|
+
dependencies
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
async function readApmManifest(baseDir) {
|
|
2101
|
+
const path2 = getApmManifestPath(baseDir);
|
|
2102
|
+
const content = await readFileContent(path2);
|
|
2103
|
+
return parseApmManifest(content);
|
|
2104
|
+
}
|
|
2105
|
+
function normalizeDependency(entry, index) {
|
|
2106
|
+
if (typeof entry === "string") {
|
|
2107
|
+
return normalizeStringDependency(entry, index);
|
|
2108
|
+
}
|
|
2109
|
+
const gitUrl = entry.git ?? entry.source;
|
|
2110
|
+
if (!gitUrl) {
|
|
2111
|
+
throw new Error(
|
|
2112
|
+
`apm.yml dependency #${index + 1}: object form requires a "git" field. Received: ${JSON.stringify(entry)}.`
|
|
2113
|
+
);
|
|
2114
|
+
}
|
|
2115
|
+
const parsedUrl = parseHttpsGitHubUrl(gitUrl);
|
|
2116
|
+
if (!parsedUrl) {
|
|
2117
|
+
throw new Error(
|
|
2118
|
+
`apm.yml dependency #${index + 1}: unsupported git URL "${gitUrl}". Only HTTPS GitHub URLs (https://github.com/owner/repo[.git]) are supported in this version. SSH, GitLab, Bitbucket, and other hosts are not yet supported.`
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
if (entry.path !== void 0) {
|
|
2122
|
+
validateSubPath(entry.path, index);
|
|
2123
|
+
}
|
|
2124
|
+
return {
|
|
2125
|
+
gitUrl: parsedUrl.gitUrl,
|
|
2126
|
+
owner: parsedUrl.owner,
|
|
2127
|
+
repo: parsedUrl.repo,
|
|
2128
|
+
ref: entry.ref,
|
|
2129
|
+
path: entry.path,
|
|
2130
|
+
alias: entry.alias
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
function validateSubPath(subPath, index) {
|
|
2134
|
+
if (subPath === "" || subPath.startsWith("/") || subPath.startsWith("\\")) {
|
|
2135
|
+
throw new Error(
|
|
2136
|
+
`apm.yml dependency #${index + 1}: "path" must be a non-empty relative path without a leading slash. Received: ${JSON.stringify(subPath)}.`
|
|
2137
|
+
);
|
|
2138
|
+
}
|
|
2139
|
+
const segments = subPath.split(/[/\\]/);
|
|
2140
|
+
if (segments.includes("..")) {
|
|
2141
|
+
throw new Error(
|
|
2142
|
+
`apm.yml dependency #${index + 1}: "path" must not contain ".." segments. Received: ${JSON.stringify(subPath)}.`
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
function normalizeStringDependency(entry, index) {
|
|
2147
|
+
const trimmed = entry.trim();
|
|
2148
|
+
if (!trimmed) {
|
|
2149
|
+
throw new Error(`apm.yml dependency #${index + 1}: entry must be a non-empty string.`);
|
|
2150
|
+
}
|
|
2151
|
+
rejectUnsupportedShorthand(trimmed, index);
|
|
2152
|
+
if (trimmed.startsWith("https://")) {
|
|
2153
|
+
const [urlPart, refPart2] = splitOnFirst(trimmed, "#");
|
|
2154
|
+
const parsed = parseHttpsGitHubUrl(urlPart);
|
|
2155
|
+
if (!parsed) {
|
|
2156
|
+
throw new Error(
|
|
2157
|
+
`apm.yml dependency #${index + 1}: unsupported URL "${urlPart}". Only HTTPS GitHub URLs (https://github.com/owner/repo[.git]) are supported in this version.`
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
return {
|
|
2161
|
+
gitUrl: parsed.gitUrl,
|
|
2162
|
+
owner: parsed.owner,
|
|
2163
|
+
repo: parsed.repo,
|
|
2164
|
+
ref: refPart2 || void 0
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
const [ownerRepo, refPart] = splitOnFirst(trimmed, "#");
|
|
2168
|
+
const slashIndex = ownerRepo.indexOf("/");
|
|
2169
|
+
if (slashIndex === -1 || slashIndex === 0 || slashIndex === ownerRepo.length - 1) {
|
|
2170
|
+
throw new Error(
|
|
2171
|
+
`apm.yml dependency #${index + 1}: shorthand "${entry}" must be in the form "owner/repo[#ref]".`
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
if (ownerRepo.includes("/", slashIndex + 1)) {
|
|
2175
|
+
throw new Error(
|
|
2176
|
+
`apm.yml dependency #${index + 1}: FQDN shorthand or sub-path shorthand ("${entry}") is not yet supported. Use the object form with an explicit "git" URL.`
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
const owner = ownerRepo.substring(0, slashIndex).toLowerCase();
|
|
2180
|
+
const repo = ownerRepo.substring(slashIndex + 1).toLowerCase();
|
|
2181
|
+
return {
|
|
2182
|
+
gitUrl: `https://github.com/${owner}/${repo}.git`,
|
|
2183
|
+
owner,
|
|
2184
|
+
repo,
|
|
2185
|
+
ref: refPart || void 0
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
function rejectUnsupportedShorthand(entry, index) {
|
|
2189
|
+
if (entry.startsWith("./") || entry.startsWith("../") || entry.startsWith("/")) {
|
|
2190
|
+
throw new Error(
|
|
2191
|
+
`apm.yml dependency #${index + 1}: local path dependencies ("${entry}") are not yet supported by rulesync.`
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
if (entry.startsWith("git@") || entry.startsWith("ssh://")) {
|
|
2195
|
+
throw new Error(
|
|
2196
|
+
`apm.yml dependency #${index + 1}: SSH URL dependencies ("${entry}") are not yet supported. Use an HTTPS GitHub URL.`
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
if (entry.includes("@marketplace")) {
|
|
2200
|
+
throw new Error(
|
|
2201
|
+
`apm.yml dependency #${index + 1}: APM marketplace dependencies ("${entry}") are not yet supported.`
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
function parseHttpsGitHubUrl(url) {
|
|
2206
|
+
let parsed;
|
|
2207
|
+
try {
|
|
2208
|
+
parsed = new URL(url);
|
|
2209
|
+
} catch {
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
const host = parsed.hostname.toLowerCase();
|
|
2213
|
+
if (host !== "github.com" && host !== "www.github.com") {
|
|
2214
|
+
return null;
|
|
2215
|
+
}
|
|
2216
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
2217
|
+
if (segments.length < 2) {
|
|
2218
|
+
return null;
|
|
2219
|
+
}
|
|
2220
|
+
const rawOwner = segments[0];
|
|
2221
|
+
const rawRepo = segments[1];
|
|
2222
|
+
if (!rawOwner || !rawRepo) {
|
|
2223
|
+
return null;
|
|
2224
|
+
}
|
|
2225
|
+
const owner = rawOwner.toLowerCase();
|
|
2226
|
+
const repo = rawRepo.replace(/\.git$/, "").toLowerCase();
|
|
2227
|
+
return {
|
|
2228
|
+
gitUrl: `https://github.com/${owner}/${repo}.git`,
|
|
2229
|
+
owner,
|
|
2230
|
+
repo
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
function splitOnFirst(input, separator) {
|
|
2234
|
+
const idx = input.indexOf(separator);
|
|
2235
|
+
if (idx === -1) return [input, void 0];
|
|
2236
|
+
return [input.substring(0, idx), input.substring(idx + 1)];
|
|
2237
|
+
}
|
|
2238
|
+
|
|
2239
|
+
// src/lib/apm/apm-install.ts
|
|
2240
|
+
var RULESYNC_APM_COMPAT_VERSION = "rulesync-compat/0.1";
|
|
2241
|
+
var APM_PRIMITIVES = [
|
|
2242
|
+
{
|
|
2243
|
+
sourceDir: ".apm/instructions",
|
|
2244
|
+
deployDir: ".github/instructions",
|
|
2245
|
+
packageType: "apm_package"
|
|
2246
|
+
},
|
|
2247
|
+
{
|
|
2248
|
+
sourceDir: ".apm/skills",
|
|
2249
|
+
deployDir: ".github/skills",
|
|
2250
|
+
packageType: "apm_package"
|
|
2251
|
+
}
|
|
2252
|
+
];
|
|
2253
|
+
async function installApm(params) {
|
|
2254
|
+
const { baseDir, options = {}, logger: logger5 } = params;
|
|
2255
|
+
const manifest = await readApmManifest(baseDir);
|
|
2256
|
+
if (manifest.dependencies.length === 0) {
|
|
2257
|
+
logger5.warn("apm.yml has no dependencies.apm entries. Nothing to install.");
|
|
2258
|
+
return { dependenciesProcessed: 0, deployedFileCount: 0, failedDependencyCount: 0 };
|
|
2259
|
+
}
|
|
2260
|
+
const existingLock = await readApmLock(baseDir);
|
|
2261
|
+
if (options.frozen) {
|
|
2262
|
+
if (!existingLock) {
|
|
2263
|
+
throw new Error(
|
|
2264
|
+
"Frozen install failed: rulesync-apm.lock.yaml is missing. Run 'rulesync install --mode apm' to create it."
|
|
2265
|
+
);
|
|
2266
|
+
}
|
|
2267
|
+
const missing = manifest.dependencies.filter(
|
|
2268
|
+
(dep) => !findApmLockDependency(existingLock, canonicalRepoUrl(dep))
|
|
2269
|
+
);
|
|
2270
|
+
if (missing.length > 0) {
|
|
2271
|
+
const names = missing.map((d) => d.gitUrl).join(", ");
|
|
2272
|
+
throw new Error(
|
|
2273
|
+
`Frozen install failed: rulesync-apm.lock.yaml is missing entries for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
const drifted = manifest.dependencies.filter((dep) => {
|
|
2277
|
+
if (dep.ref === void 0) return false;
|
|
2278
|
+
const locked = findApmLockDependency(existingLock, canonicalRepoUrl(dep));
|
|
2279
|
+
return locked?.resolved_ref !== void 0 && locked.resolved_ref !== dep.ref;
|
|
2280
|
+
});
|
|
2281
|
+
if (drifted.length > 0) {
|
|
2282
|
+
const names = drifted.map((d) => {
|
|
2283
|
+
const locked = findApmLockDependency(existingLock, canonicalRepoUrl(d));
|
|
2284
|
+
return `${d.gitUrl} (manifest=${d.ref}, lock=${locked?.resolved_ref})`;
|
|
2285
|
+
}).join(", ");
|
|
2286
|
+
throw new Error(
|
|
2287
|
+
`Frozen install failed: manifest ref does not match rulesync-apm.lock.yaml for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
|
|
2288
|
+
);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
const token = GitHubClient.resolveToken(options.token);
|
|
2292
|
+
const client = new GitHubClient({ token });
|
|
2293
|
+
const semaphore = new Semaphore2(FETCH_CONCURRENCY_LIMIT);
|
|
2294
|
+
const newLock = createEmptyApmLock({
|
|
2295
|
+
apmVersion: existingLock?.apm_version ?? RULESYNC_APM_COMPAT_VERSION,
|
|
2296
|
+
existingLock
|
|
2297
|
+
});
|
|
2298
|
+
const frozen = options.frozen ?? false;
|
|
2299
|
+
const runOne = async (dep) => {
|
|
2300
|
+
const installed = await installDependency({
|
|
2301
|
+
dep,
|
|
2302
|
+
client,
|
|
2303
|
+
semaphore,
|
|
2304
|
+
baseDir,
|
|
2305
|
+
existingLock,
|
|
2306
|
+
frozen,
|
|
2307
|
+
update: options.update ?? false,
|
|
2308
|
+
logger: logger5
|
|
2309
|
+
});
|
|
2310
|
+
return {
|
|
2311
|
+
status: "ok",
|
|
2312
|
+
lockEntry: installed.lockEntry,
|
|
2313
|
+
deployedCount: installed.deployedFiles.length
|
|
2314
|
+
};
|
|
2315
|
+
};
|
|
2316
|
+
const results = frozen ? await Promise.all(manifest.dependencies.map(runOne)) : await Promise.all(
|
|
2317
|
+
manifest.dependencies.map(async (dep) => {
|
|
2318
|
+
try {
|
|
2319
|
+
return await runOne(dep);
|
|
2320
|
+
} catch (error) {
|
|
2321
|
+
logger5.error(`Failed to install apm dependency "${dep.gitUrl}": ${formatError(error)}`);
|
|
2322
|
+
if (error instanceof GitHubClientError) {
|
|
2323
|
+
logGitHubAuthHints({ error, logger: logger5 });
|
|
2324
|
+
}
|
|
2325
|
+
const previous = existingLock ? findApmLockDependency(existingLock, canonicalRepoUrl(dep)) : void 0;
|
|
2326
|
+
return { status: "failed", previous };
|
|
2327
|
+
}
|
|
2328
|
+
})
|
|
2329
|
+
);
|
|
2330
|
+
let totalDeployed = 0;
|
|
2331
|
+
let failedCount = 0;
|
|
2332
|
+
for (const result of results) {
|
|
2333
|
+
if (result.status === "ok") {
|
|
2334
|
+
newLock.dependencies.push(result.lockEntry);
|
|
2335
|
+
totalDeployed += result.deployedCount;
|
|
2336
|
+
} else {
|
|
2337
|
+
failedCount += 1;
|
|
2338
|
+
if (result.previous) {
|
|
2339
|
+
newLock.dependencies.push(result.previous);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
if (existingLock) {
|
|
2344
|
+
const newDeployedFiles = new Set(newLock.dependencies.flatMap((d) => d.deployed_files));
|
|
2345
|
+
const toDelete = [];
|
|
2346
|
+
for (const prev of existingLock.dependencies) {
|
|
2347
|
+
for (const deployed of prev.deployed_files) {
|
|
2348
|
+
if (!newDeployedFiles.has(deployed)) {
|
|
2349
|
+
toDelete.push(deployed);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
for (const relativePath of toDelete) {
|
|
2354
|
+
if (posix2.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
|
|
2355
|
+
logger5.warn(`Refusing to remove stale apm file with suspicious path: "${relativePath}".`);
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
try {
|
|
2359
|
+
checkPathTraversal({ relativePath, intendedRootDir: baseDir });
|
|
2360
|
+
} catch {
|
|
2361
|
+
logger5.warn(`Refusing to remove stale apm file outside baseDir: "${relativePath}".`);
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
const absolute = join6(baseDir, relativePath);
|
|
2365
|
+
await removeFile(absolute);
|
|
2366
|
+
logger5.debug(`Removed stale apm file: ${relativePath}`);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
if (!frozen) {
|
|
2370
|
+
newLock.generated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2371
|
+
await writeApmLock({ baseDir, lock: newLock });
|
|
2372
|
+
if (failedCount === 0) {
|
|
2373
|
+
logger5.debug("rulesync-apm.lock.yaml updated.");
|
|
2374
|
+
} else {
|
|
2375
|
+
logger5.warn(
|
|
2376
|
+
`rulesync-apm.lock.yaml written with partially successful installs (${failedCount} dep(s) failed).`
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
return {
|
|
2381
|
+
dependenciesProcessed: manifest.dependencies.length,
|
|
2382
|
+
deployedFileCount: totalDeployed,
|
|
2383
|
+
failedDependencyCount: failedCount
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
async function installDependency(params) {
|
|
2387
|
+
const { dep, client, semaphore, baseDir, existingLock, frozen, update, logger: logger5 } = params;
|
|
2388
|
+
const repoUrl = canonicalRepoUrl(dep);
|
|
2389
|
+
const locked = existingLock ? findApmLockDependency(existingLock, repoUrl) : void 0;
|
|
2390
|
+
let resolvedRef;
|
|
2391
|
+
let resolvedSha;
|
|
2392
|
+
if (locked && !update && locked.resolved_commit && locked.resolved_ref) {
|
|
2393
|
+
resolvedRef = locked.resolved_ref;
|
|
2394
|
+
resolvedSha = locked.resolved_commit;
|
|
2395
|
+
logger5.debug(`Using locked commit for ${repoUrl}: ${resolvedSha}`);
|
|
2396
|
+
} else {
|
|
2397
|
+
resolvedRef = dep.ref ?? await client.getDefaultBranch(dep.owner, dep.repo);
|
|
2398
|
+
resolvedSha = await client.resolveRefToSha(dep.owner, dep.repo, resolvedRef);
|
|
2399
|
+
logger5.debug(`Resolved ${repoUrl} ref "${resolvedRef}" -> ${resolvedSha}`);
|
|
2400
|
+
}
|
|
2401
|
+
const deployed = [];
|
|
2402
|
+
for (const primitive of APM_PRIMITIVES) {
|
|
2403
|
+
const remoteBase = dep.path ? toPosixPath(posix2.join(dep.path, primitive.sourceDir)) : primitive.sourceDir;
|
|
2404
|
+
const files = await listPrimitiveFiles({
|
|
2405
|
+
client,
|
|
2406
|
+
semaphore,
|
|
2407
|
+
owner: dep.owner,
|
|
2408
|
+
repo: dep.repo,
|
|
2409
|
+
ref: resolvedSha,
|
|
2410
|
+
remoteBase,
|
|
2411
|
+
logger: logger5
|
|
2412
|
+
});
|
|
2413
|
+
if (files.length === 0) continue;
|
|
2414
|
+
for (const file of files) {
|
|
2415
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
2416
|
+
logger5.warn(
|
|
2417
|
+
`Skipping "${file.path}" from ${repoUrl}: ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
2418
|
+
);
|
|
2419
|
+
continue;
|
|
2420
|
+
}
|
|
2421
|
+
const relativeToBase = posix2.relative(remoteBase, toPosixPath(file.path));
|
|
2422
|
+
if (!relativeToBase || relativeToBase.startsWith("..") || posix2.isAbsolute(relativeToBase)) {
|
|
2423
|
+
logger5.warn(
|
|
2424
|
+
`Skipping "${file.path}" from ${repoUrl}: resolved outside of "${remoteBase}".`
|
|
2425
|
+
);
|
|
2426
|
+
continue;
|
|
2427
|
+
}
|
|
2428
|
+
const deployRelative = toPosixPath(join6(primitive.deployDir, relativeToBase));
|
|
2429
|
+
checkPathTraversal({
|
|
2430
|
+
relativePath: deployRelative,
|
|
2431
|
+
intendedRootDir: baseDir
|
|
2432
|
+
});
|
|
2433
|
+
const content = await withSemaphore(
|
|
2434
|
+
semaphore,
|
|
2435
|
+
() => client.getFileContent(dep.owner, dep.repo, file.path, resolvedSha)
|
|
2436
|
+
);
|
|
2437
|
+
const byteLength = Buffer.byteLength(content, "utf8");
|
|
2438
|
+
if (byteLength > MAX_FILE_SIZE) {
|
|
2439
|
+
logger5.warn(
|
|
2440
|
+
`Skipping "${file.path}" from ${repoUrl}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
2441
|
+
);
|
|
2442
|
+
continue;
|
|
2443
|
+
}
|
|
2444
|
+
deployed.push({ path: deployRelative, content });
|
|
2445
|
+
if (!frozen) {
|
|
2446
|
+
await writeFileContent(join6(baseDir, deployRelative), content);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
deployed.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
2451
|
+
const deployedFiles = deployed.map((d) => d.path);
|
|
2452
|
+
const contentHash = computeContentHash(deployed);
|
|
2453
|
+
if (frozen && locked?.content_hash) {
|
|
2454
|
+
if (RULESYNC_CONTENT_HASH_REGEX.test(locked.content_hash)) {
|
|
2455
|
+
if (locked.content_hash !== contentHash) {
|
|
2456
|
+
throw new Error(
|
|
2457
|
+
`content_hash mismatch for ${repoUrl}: lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
2460
|
+
} else {
|
|
2461
|
+
logger5.debug(
|
|
2462
|
+
`Skipping content_hash integrity check for ${repoUrl}: recorded hash "${locked.content_hash}" was not written by rulesync.`
|
|
2463
|
+
);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
if (frozen) {
|
|
2467
|
+
for (const { path: deployRelative, content } of deployed) {
|
|
2468
|
+
await writeFileContent(join6(baseDir, deployRelative), content);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
const lockEntry = {
|
|
2472
|
+
repo_url: repoUrl,
|
|
2473
|
+
resolved_commit: resolvedSha,
|
|
2474
|
+
resolved_ref: resolvedRef,
|
|
2475
|
+
depth: 1,
|
|
2476
|
+
package_type: "apm_package",
|
|
2477
|
+
content_hash: contentHash,
|
|
2478
|
+
deployed_files: deployedFiles
|
|
2479
|
+
};
|
|
2480
|
+
if (dep.path) {
|
|
2481
|
+
lockEntry.virtual_path = dep.path;
|
|
2482
|
+
}
|
|
2483
|
+
logger5.info(`Installed ${deployedFiles.length} file(s) from ${repoUrl}@${shortSha(resolvedSha)}`);
|
|
2484
|
+
return { lockEntry, deployedFiles };
|
|
2485
|
+
}
|
|
2486
|
+
function computeContentHash(files) {
|
|
2487
|
+
const hash = createHash("sha256");
|
|
2488
|
+
for (const { path: path2, content } of files) {
|
|
2489
|
+
hash.update(path2);
|
|
2490
|
+
hash.update("\0");
|
|
2491
|
+
hash.update(content);
|
|
2492
|
+
hash.update("\0");
|
|
2493
|
+
}
|
|
2494
|
+
return `sha256:${hash.digest("hex")}`;
|
|
2495
|
+
}
|
|
2496
|
+
async function listPrimitiveFiles(params) {
|
|
2497
|
+
const { client, semaphore, owner, repo, ref, remoteBase, logger: logger5 } = params;
|
|
2498
|
+
try {
|
|
2499
|
+
return await listDirectoryRecursive({
|
|
2500
|
+
client,
|
|
2501
|
+
owner,
|
|
2502
|
+
repo,
|
|
2503
|
+
path: remoteBase,
|
|
2504
|
+
ref,
|
|
2505
|
+
semaphore
|
|
2506
|
+
});
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
if (error instanceof GitHubClientError && error.statusCode === 404) {
|
|
2509
|
+
logger5.debug(`No ${remoteBase}/ in ${owner}/${repo}, skipping.`);
|
|
2510
|
+
return [];
|
|
2511
|
+
}
|
|
2512
|
+
throw error;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
function canonicalRepoUrl(dep) {
|
|
2516
|
+
return `https://github.com/${dep.owner}/${dep.repo}`;
|
|
2517
|
+
}
|
|
2518
|
+
function shortSha(sha) {
|
|
2519
|
+
return sha.substring(0, 7);
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
// src/lib/gh/gh-install.ts
|
|
2523
|
+
import { createHash as createHash2 } from "crypto";
|
|
2524
|
+
import { basename, join as join9, posix as posix3 } from "path";
|
|
2525
|
+
import { Semaphore as Semaphore3 } from "es-toolkit/promise";
|
|
2526
|
+
|
|
2527
|
+
// src/lib/gh/gh-frontmatter.ts
|
|
2528
|
+
import { dump as dump3, load as load3 } from "js-yaml";
|
|
2529
|
+
var FRONTMATTER_FENCE = "---";
|
|
2530
|
+
function injectSourceMetadata(params) {
|
|
2531
|
+
const { content, source, repository, ref } = params;
|
|
2532
|
+
const provenance = { source, repository, ref };
|
|
2533
|
+
let openFenceLen;
|
|
2534
|
+
if (content.startsWith(`${FRONTMATTER_FENCE}\r
|
|
2535
|
+
`)) {
|
|
2536
|
+
openFenceLen = 5;
|
|
2537
|
+
} else if (content.startsWith(`${FRONTMATTER_FENCE}
|
|
2538
|
+
`)) {
|
|
2539
|
+
openFenceLen = 4;
|
|
2540
|
+
} else if (content === FRONTMATTER_FENCE) {
|
|
2541
|
+
openFenceLen = 3;
|
|
2542
|
+
} else {
|
|
2543
|
+
const yaml2 = dump3(provenance, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2544
|
+
return `${FRONTMATTER_FENCE}
|
|
2545
|
+
${yaml2}${FRONTMATTER_FENCE}
|
|
2546
|
+
${content}`;
|
|
2547
|
+
}
|
|
2548
|
+
const afterOpen = content.substring(openFenceLen);
|
|
2549
|
+
let fmBody;
|
|
2550
|
+
let rest;
|
|
2551
|
+
if (afterOpen.startsWith("---\n") || afterOpen.startsWith("---\r\n") || afterOpen === "---") {
|
|
2552
|
+
fmBody = "";
|
|
2553
|
+
const fenceLen = afterOpen.startsWith("---\r\n") ? 5 : afterOpen === "---" ? 3 : 4;
|
|
2554
|
+
rest = afterOpen.substring(fenceLen);
|
|
2555
|
+
} else {
|
|
2556
|
+
const match = /\n---(\r?\n|$)/.exec(afterOpen);
|
|
2557
|
+
if (!match) {
|
|
2558
|
+
throw new Error("invalid frontmatter");
|
|
2559
|
+
}
|
|
2560
|
+
fmBody = afterOpen.substring(0, match.index);
|
|
2561
|
+
rest = afterOpen.substring(match.index + match[0].length);
|
|
2562
|
+
}
|
|
2563
|
+
let loaded;
|
|
2564
|
+
try {
|
|
2565
|
+
loaded = load3(fmBody);
|
|
2566
|
+
} catch {
|
|
2567
|
+
throw new Error("invalid frontmatter");
|
|
2568
|
+
}
|
|
2569
|
+
if (loaded === null || loaded === void 0) {
|
|
2570
|
+
const yaml2 = dump3(provenance, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2571
|
+
return `${FRONTMATTER_FENCE}
|
|
2572
|
+
${yaml2}${FRONTMATTER_FENCE}
|
|
2573
|
+
${rest}`;
|
|
2574
|
+
}
|
|
2575
|
+
if (typeof loaded !== "object" || Array.isArray(loaded)) {
|
|
2576
|
+
throw new Error("invalid frontmatter");
|
|
2577
|
+
}
|
|
2578
|
+
const existing = loaded;
|
|
2579
|
+
const merged = {
|
|
2580
|
+
...existing,
|
|
2581
|
+
...provenance
|
|
2582
|
+
};
|
|
2583
|
+
const yaml = dump3(merged, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2584
|
+
return `${FRONTMATTER_FENCE}
|
|
2585
|
+
${yaml}${FRONTMATTER_FENCE}
|
|
2586
|
+
${rest}`;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// src/lib/gh/gh-lock.ts
|
|
2590
|
+
import { join as join7 } from "path";
|
|
2591
|
+
import { dump as dump4, load as load4 } from "js-yaml";
|
|
2592
|
+
import { optional as optional3, refine as refine2, z as z6 } from "zod/mini";
|
|
2593
|
+
var GH_LOCKFILE_FILE_NAME = "rulesync-gh.lock.yaml";
|
|
2594
|
+
var GH_LOCKFILE_VERSION = "1";
|
|
2595
|
+
var RULESYNC_CONTENT_HASH_REGEX2 = /^sha256:[0-9a-f]{64}$/;
|
|
2596
|
+
var ScopeSchema = z6.enum(["project", "user"]);
|
|
2597
|
+
var GhLockInstallationSchema = z6.looseObject({
|
|
2598
|
+
source: z6.string(),
|
|
2599
|
+
owner: z6.string(),
|
|
2600
|
+
repo: z6.string(),
|
|
2601
|
+
agent: z6.string(),
|
|
2602
|
+
scope: ScopeSchema,
|
|
2603
|
+
skill: z6.string(),
|
|
2604
|
+
requested_ref: optional3(z6.string()),
|
|
2605
|
+
resolved_ref: z6.string(),
|
|
2606
|
+
resolved_commit: z6.string().check(refine2((v) => /^[0-9a-f]{40}$/.test(v), "resolved_commit must be a 40-char hex SHA")),
|
|
2607
|
+
install_dir: z6.string(),
|
|
2608
|
+
deployed_files: z6.array(z6.string()),
|
|
2609
|
+
content_hash: optional3(z6.string())
|
|
2610
|
+
});
|
|
2611
|
+
var GhLockSchema = z6.looseObject({
|
|
2612
|
+
lockfile_version: z6.literal("1"),
|
|
2613
|
+
generated_at: z6.string(),
|
|
2614
|
+
installations: z6.array(GhLockInstallationSchema)
|
|
2615
|
+
});
|
|
2616
|
+
function getGhLockPath(baseDir) {
|
|
2617
|
+
return join7(baseDir, GH_LOCKFILE_FILE_NAME);
|
|
2618
|
+
}
|
|
2619
|
+
function createEmptyGhLock(params) {
|
|
2620
|
+
const base = params?.existingLock ? { ...params.existingLock } : {};
|
|
2621
|
+
return {
|
|
2622
|
+
...base,
|
|
2623
|
+
lockfile_version: GH_LOCKFILE_VERSION,
|
|
2624
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2625
|
+
installations: []
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
function parseGhLock(content) {
|
|
2629
|
+
if (!content.trim()) {
|
|
2630
|
+
return null;
|
|
2631
|
+
}
|
|
2632
|
+
let loaded;
|
|
2633
|
+
try {
|
|
2634
|
+
loaded = load4(content);
|
|
2635
|
+
} catch {
|
|
2636
|
+
return null;
|
|
2637
|
+
}
|
|
2638
|
+
if (!loaded || typeof loaded !== "object") {
|
|
2639
|
+
return null;
|
|
2640
|
+
}
|
|
2641
|
+
const parsed = GhLockSchema.safeParse(loaded);
|
|
2642
|
+
if (!parsed.success) {
|
|
2643
|
+
const issues = parsed.error.issues.map((issue) => ` - ${issue.path.join(".") || "<root>"}: ${issue.message}`).join("\n");
|
|
2644
|
+
throw new Error(`Invalid ${GH_LOCKFILE_FILE_NAME}:
|
|
2645
|
+
${issues}`);
|
|
2646
|
+
}
|
|
2647
|
+
return parsed.data;
|
|
2648
|
+
}
|
|
2649
|
+
async function readGhLock(baseDir) {
|
|
2650
|
+
const path2 = getGhLockPath(baseDir);
|
|
2651
|
+
if (!await fileExists(path2)) {
|
|
2652
|
+
return null;
|
|
2653
|
+
}
|
|
2654
|
+
const content = await readFileContent(path2);
|
|
2655
|
+
return parseGhLock(content);
|
|
2656
|
+
}
|
|
2657
|
+
async function writeGhLock(params) {
|
|
2658
|
+
const path2 = getGhLockPath(params.baseDir);
|
|
2659
|
+
const content = serializeGhLock(params.lock);
|
|
2660
|
+
await writeFileContent(path2, content);
|
|
2661
|
+
}
|
|
2662
|
+
function serializeGhLock(lock) {
|
|
2663
|
+
return dump4(lock, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2664
|
+
}
|
|
2665
|
+
function findGhLockInstallation(lock, params) {
|
|
2666
|
+
const target = params.source.toLowerCase();
|
|
2667
|
+
return lock.installations.find(
|
|
2668
|
+
(i) => i.source.toLowerCase() === target && i.agent === params.agent && i.scope === params.scope && i.skill === params.skill
|
|
2669
|
+
);
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// src/lib/gh/gh-paths.ts
|
|
2673
|
+
import { join as join8 } from "path";
|
|
2674
|
+
var GH_AGENTS = [
|
|
2675
|
+
"github-copilot",
|
|
2676
|
+
"claude-code",
|
|
2677
|
+
"cursor",
|
|
2678
|
+
"codex",
|
|
2679
|
+
"gemini",
|
|
2680
|
+
"antigravity"
|
|
2681
|
+
];
|
|
2682
|
+
function relativeInstallDirFor(params) {
|
|
2683
|
+
const { agent, scope } = params;
|
|
2684
|
+
if (scope === "project") {
|
|
2685
|
+
if (agent === "claude-code") {
|
|
2686
|
+
return join8(".claude", "skills");
|
|
2687
|
+
}
|
|
2688
|
+
return join8(".agents", "skills");
|
|
2689
|
+
}
|
|
2690
|
+
switch (agent) {
|
|
2691
|
+
case "github-copilot":
|
|
2692
|
+
return join8(".copilot", "skills");
|
|
2693
|
+
case "claude-code":
|
|
2694
|
+
return join8(".claude", "skills");
|
|
2695
|
+
case "cursor":
|
|
2696
|
+
return join8(".cursor", "skills");
|
|
2697
|
+
case "codex":
|
|
2698
|
+
return join8(".codex", "skills");
|
|
2699
|
+
case "gemini":
|
|
2700
|
+
return join8(".gemini", "skills");
|
|
2701
|
+
case "antigravity":
|
|
2702
|
+
return join8(".gemini", "antigravity", "skills");
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
// src/lib/gh/gh-install.ts
|
|
2707
|
+
var SKILLS_REMOTE_DIR = "skills";
|
|
2708
|
+
var SKILL_FILE_NAME2 = "SKILL.md";
|
|
2709
|
+
async function installGh(params) {
|
|
2710
|
+
const { baseDir, sources, options = {}, logger: logger5 } = params;
|
|
2711
|
+
if (sources.length === 0) {
|
|
2712
|
+
return { sourcesProcessed: 0, installedSkillCount: 0, failedSourceCount: 0 };
|
|
2713
|
+
}
|
|
2714
|
+
const resolvedSources = sources.map((entry) => {
|
|
2715
|
+
const parsed = parseSource(entry.source);
|
|
2716
|
+
if (parsed.provider !== "github") {
|
|
2717
|
+
throw new Error(
|
|
2718
|
+
`--mode gh only supports GitHub sources. "${entry.source}" resolves to provider "${parsed.provider}".`
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2721
|
+
if (entry.transport !== void 0 && entry.transport !== "github") {
|
|
2722
|
+
throw new Error(
|
|
2723
|
+
`--mode gh: field "transport" is not supported (got "${entry.transport}" for source "${entry.source}"). Drop the field or switch to --mode rulesync.`
|
|
2724
|
+
);
|
|
2725
|
+
}
|
|
2726
|
+
if (entry.path !== void 0) {
|
|
2727
|
+
throw new Error(
|
|
2728
|
+
`--mode gh: field "path" is not supported for source "${entry.source}". The remote layout is fixed to "skills/<name>/SKILL.md".`
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
const agent = entry.agent ?? "github-copilot";
|
|
2732
|
+
if (!GH_AGENTS.includes(agent)) {
|
|
2733
|
+
throw new Error(
|
|
2734
|
+
`--mode gh: unknown agent "${agent}" for source "${entry.source}". Valid agents: ${GH_AGENTS.join(", ")}.`
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
const scope = entry.scope ?? "project";
|
|
2738
|
+
return {
|
|
2739
|
+
entry,
|
|
2740
|
+
owner: parsed.owner,
|
|
2741
|
+
repo: parsed.repo,
|
|
2742
|
+
ref: entry.ref ?? parsed.ref,
|
|
2743
|
+
agent,
|
|
2744
|
+
scope
|
|
2745
|
+
};
|
|
2746
|
+
});
|
|
2747
|
+
const existingLock = await readGhLock(baseDir);
|
|
2748
|
+
const frozen = options.frozen ?? false;
|
|
2749
|
+
const update = options.update ?? false;
|
|
2750
|
+
if (frozen && !existingLock) {
|
|
2751
|
+
throw new Error(
|
|
2752
|
+
"Frozen install failed: rulesync-gh.lock.yaml is missing. Run 'rulesync install --mode gh' to create it."
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2755
|
+
if (frozen && existingLock) {
|
|
2756
|
+
const uncovered = [];
|
|
2757
|
+
for (const rs of resolvedSources) {
|
|
2758
|
+
const hasAny = existingLock.installations.some(
|
|
2759
|
+
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase() && i.agent === rs.agent && i.scope === rs.scope
|
|
2760
|
+
);
|
|
2761
|
+
if (!hasAny) {
|
|
2762
|
+
uncovered.push(`${rs.entry.source} (agent=${rs.agent}, scope=${rs.scope})`);
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
if (uncovered.length > 0) {
|
|
2766
|
+
throw new Error(
|
|
2767
|
+
`Frozen install failed: rulesync-gh.lock.yaml is missing entries for: ${uncovered.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
2768
|
+
);
|
|
2769
|
+
}
|
|
2770
|
+
const drifted = [];
|
|
2771
|
+
for (const rs of resolvedSources) {
|
|
2772
|
+
if (!rs.ref) continue;
|
|
2773
|
+
const matches = existingLock.installations.filter(
|
|
2774
|
+
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase()
|
|
2775
|
+
);
|
|
2776
|
+
for (const m of matches) {
|
|
2777
|
+
if (m.requested_ref !== void 0 && m.requested_ref !== rs.ref) {
|
|
2778
|
+
drifted.push(`${rs.entry.source} (manifest=${rs.ref}, lock=${m.requested_ref})`);
|
|
2779
|
+
break;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
if (drifted.length > 0) {
|
|
2784
|
+
throw new Error(
|
|
2785
|
+
`Frozen install failed: manifest ref does not match rulesync-gh.lock.yaml for: ${drifted.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
2786
|
+
);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
const token = GitHubClient.resolveToken(options.token);
|
|
2790
|
+
const client = new GitHubClient({ token });
|
|
2791
|
+
const semaphore = new Semaphore3(FETCH_CONCURRENCY_LIMIT);
|
|
2792
|
+
const newLock = createEmptyGhLock({ existingLock });
|
|
2793
|
+
const runOne = async (rs) => {
|
|
2794
|
+
const installations = await installSource({
|
|
2795
|
+
rs,
|
|
2796
|
+
client,
|
|
2797
|
+
semaphore,
|
|
2798
|
+
baseDir,
|
|
2799
|
+
existingLock,
|
|
2800
|
+
frozen,
|
|
2801
|
+
update,
|
|
2802
|
+
logger: logger5
|
|
2803
|
+
});
|
|
2804
|
+
return { status: "ok", installations };
|
|
2805
|
+
};
|
|
2806
|
+
const results = frozen ? await Promise.all(resolvedSources.map(runOne)) : await Promise.all(
|
|
2807
|
+
resolvedSources.map(async (rs) => {
|
|
2808
|
+
try {
|
|
2809
|
+
return await runOne(rs);
|
|
2810
|
+
} catch (error) {
|
|
2811
|
+
logger5.error(`Failed to install gh source "${rs.entry.source}": ${formatError(error)}`);
|
|
2812
|
+
if (error instanceof GitHubClientError) {
|
|
2813
|
+
logGitHubAuthHints({ error, logger: logger5 });
|
|
2814
|
+
}
|
|
2815
|
+
const preserved = existingLock ? existingLock.installations.filter(
|
|
2816
|
+
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase()
|
|
2817
|
+
) : [];
|
|
2818
|
+
return { status: "failed", preserved };
|
|
2819
|
+
}
|
|
2820
|
+
})
|
|
2821
|
+
);
|
|
2822
|
+
if (frozen) {
|
|
2823
|
+
for (const result of results) {
|
|
2824
|
+
if (result.status !== "ok") continue;
|
|
2825
|
+
for (const inst of result.installations) {
|
|
2826
|
+
for (const d of inst.deployed) {
|
|
2827
|
+
await writeFileContent(d.absolutePath, d.content);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
let totalInstalled = 0;
|
|
2833
|
+
let failedCount = 0;
|
|
2834
|
+
for (const result of results) {
|
|
2835
|
+
if (result.status === "ok") {
|
|
2836
|
+
for (const inst of result.installations) {
|
|
2837
|
+
newLock.installations.push(inst.installation);
|
|
2838
|
+
}
|
|
2839
|
+
totalInstalled += result.installations.length;
|
|
2840
|
+
} else {
|
|
2841
|
+
failedCount += 1;
|
|
2842
|
+
for (const preserved of result.preserved) {
|
|
2843
|
+
newLock.installations.push(preserved);
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
if (existingLock) {
|
|
2848
|
+
const newDeployed = /* @__PURE__ */ new Set();
|
|
2849
|
+
for (const inst of newLock.installations) {
|
|
2850
|
+
for (const file of inst.deployed_files) {
|
|
2851
|
+
newDeployed.add(`${inst.scope}::${file}`);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
for (const prev of existingLock.installations) {
|
|
2855
|
+
for (const deployed of prev.deployed_files) {
|
|
2856
|
+
const key = `${prev.scope}::${deployed}`;
|
|
2857
|
+
if (newDeployed.has(key)) continue;
|
|
2858
|
+
await removeStaleFile({
|
|
2859
|
+
relativePath: deployed,
|
|
2860
|
+
scope: prev.scope === "user" ? "user" : "project",
|
|
2861
|
+
baseDir,
|
|
2862
|
+
logger: logger5
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
}
|
|
2867
|
+
if (!frozen) {
|
|
2868
|
+
newLock.generated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2869
|
+
await writeGhLock({ baseDir, lock: newLock });
|
|
2870
|
+
if (failedCount === 0) {
|
|
2871
|
+
logger5.debug("rulesync-gh.lock.yaml updated.");
|
|
2872
|
+
} else {
|
|
2873
|
+
logger5.warn(
|
|
2874
|
+
`rulesync-gh.lock.yaml written with partially successful installs (${failedCount} source(s) failed).`
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
return {
|
|
2879
|
+
sourcesProcessed: sources.length,
|
|
2880
|
+
installedSkillCount: totalInstalled,
|
|
2881
|
+
failedSourceCount: failedCount
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
async function installSource(params) {
|
|
2885
|
+
const { rs, client, semaphore, baseDir, existingLock, frozen, update, logger: logger5 } = params;
|
|
2886
|
+
const { entry, owner, repo, agent, scope } = rs;
|
|
2887
|
+
const sourceKey = entry.source;
|
|
2888
|
+
let resolvedRef;
|
|
2889
|
+
let usedTag = false;
|
|
2890
|
+
if (rs.ref) {
|
|
2891
|
+
resolvedRef = rs.ref;
|
|
2892
|
+
} else {
|
|
2893
|
+
try {
|
|
2894
|
+
const release = await client.getLatestRelease(owner, repo);
|
|
2895
|
+
resolvedRef = release.tag_name;
|
|
2896
|
+
usedTag = true;
|
|
2897
|
+
} catch (error) {
|
|
2898
|
+
if (is404(error)) {
|
|
2899
|
+
resolvedRef = await client.getDefaultBranch(owner, repo);
|
|
2900
|
+
} else {
|
|
2901
|
+
throw error;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
const resolvedSha = await client.resolveRefToSha(owner, repo, resolvedRef);
|
|
2906
|
+
logger5.debug(`Resolved ${sourceKey} -> ref=${resolvedRef} sha=${resolvedSha}`);
|
|
2907
|
+
let topLevel;
|
|
2908
|
+
try {
|
|
2909
|
+
topLevel = await client.listDirectory(owner, repo, SKILLS_REMOTE_DIR, resolvedSha);
|
|
2910
|
+
} catch (error) {
|
|
2911
|
+
if (is404(error)) {
|
|
2912
|
+
logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
|
|
2913
|
+
return [];
|
|
2914
|
+
}
|
|
2915
|
+
throw error;
|
|
2916
|
+
}
|
|
2917
|
+
const skillDirs = topLevel.filter((e) => e.type === "dir").map((e) => ({ name: e.name, path: e.path }));
|
|
2918
|
+
const validatedSkills = [];
|
|
2919
|
+
for (const sk of skillDirs) {
|
|
2920
|
+
const info = await withSemaphore(
|
|
2921
|
+
semaphore,
|
|
2922
|
+
() => client.getFileInfo(owner, repo, posix3.join(sk.path, SKILL_FILE_NAME2), resolvedSha)
|
|
2923
|
+
);
|
|
2924
|
+
if (info) {
|
|
2925
|
+
validatedSkills.push(sk);
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
let selected = validatedSkills;
|
|
2929
|
+
if (entry.skills && entry.skills.length > 0) {
|
|
2930
|
+
const requested = new Set(entry.skills);
|
|
2931
|
+
selected = validatedSkills.filter((s) => requested.has(s.name));
|
|
2932
|
+
const presentNames = new Set(validatedSkills.map((s) => s.name));
|
|
2933
|
+
for (const want of entry.skills) {
|
|
2934
|
+
if (!presentNames.has(want)) {
|
|
2935
|
+
logger5.warn(`Requested skill "${want}" not found in ${sourceKey} under skills/. Skipping.`);
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
if (frozen && existingLock) {
|
|
2940
|
+
const missing = [];
|
|
2941
|
+
for (const sk of selected) {
|
|
2942
|
+
const locked = findGhLockInstallation(existingLock, {
|
|
2943
|
+
source: sourceKey,
|
|
2944
|
+
agent,
|
|
2945
|
+
scope,
|
|
2946
|
+
skill: sk.name
|
|
2947
|
+
});
|
|
2948
|
+
if (!locked) {
|
|
2949
|
+
missing.push(sk.name);
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
if (missing.length > 0) {
|
|
2953
|
+
throw new Error(
|
|
2954
|
+
`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.`
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
const results = [];
|
|
2959
|
+
const installRelDir = relativeInstallDirFor({ agent, scope });
|
|
2960
|
+
const scopeRoot = scope === "user" ? getHomeDirectory() : baseDir;
|
|
2961
|
+
const sourceUrl = `https://github.com/${owner}/${repo}`;
|
|
2962
|
+
const repository = `${owner}/${repo}`;
|
|
2963
|
+
const provenanceRef = usedTag ? resolvedRef : resolvedSha;
|
|
2964
|
+
for (const sk of selected) {
|
|
2965
|
+
const locked = existingLock && !update ? findGhLockInstallation(existingLock, {
|
|
2966
|
+
source: sourceKey,
|
|
2967
|
+
agent,
|
|
2968
|
+
scope,
|
|
2969
|
+
skill: sk.name
|
|
2970
|
+
}) : void 0;
|
|
2971
|
+
const allFiles = await listDirectoryRecursive({
|
|
2972
|
+
client,
|
|
2973
|
+
owner,
|
|
2974
|
+
repo,
|
|
2975
|
+
path: sk.path,
|
|
2976
|
+
ref: resolvedSha,
|
|
2977
|
+
semaphore
|
|
2978
|
+
});
|
|
2979
|
+
const deployed = [];
|
|
2980
|
+
for (const file of allFiles) {
|
|
2981
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
2982
|
+
logger5.warn(
|
|
2983
|
+
`Skipping "${file.path}" from ${sourceKey}: ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
2984
|
+
);
|
|
2985
|
+
continue;
|
|
2986
|
+
}
|
|
2987
|
+
const relativeToSkill = posix3.relative(sk.path, toPosixPath(file.path));
|
|
2988
|
+
if (!relativeToSkill || relativeToSkill.startsWith("..") || posix3.isAbsolute(relativeToSkill)) {
|
|
2989
|
+
logger5.warn(`Skipping "${file.path}" from ${sourceKey}: resolved outside of "${sk.path}".`);
|
|
2990
|
+
continue;
|
|
2991
|
+
}
|
|
2992
|
+
const deployRelative = toPosixPath(join9(installRelDir, sk.name, relativeToSkill));
|
|
2993
|
+
checkPathTraversal({ relativePath: deployRelative, intendedRootDir: scopeRoot });
|
|
2994
|
+
const installAbs = join9(scopeRoot, installRelDir);
|
|
2995
|
+
const withinInstallDir = toPosixPath(join9(sk.name, relativeToSkill));
|
|
2996
|
+
checkPathTraversal({ relativePath: withinInstallDir, intendedRootDir: installAbs });
|
|
2997
|
+
let content = await withSemaphore(
|
|
2998
|
+
semaphore,
|
|
2999
|
+
() => client.getFileContent(owner, repo, file.path, resolvedSha)
|
|
3000
|
+
);
|
|
3001
|
+
const byteLength = Buffer.byteLength(content, "utf8");
|
|
3002
|
+
if (byteLength > MAX_FILE_SIZE) {
|
|
3003
|
+
logger5.warn(
|
|
3004
|
+
`Skipping "${file.path}" from ${sourceKey}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
3005
|
+
);
|
|
3006
|
+
continue;
|
|
3007
|
+
}
|
|
3008
|
+
if (basename(file.path) === SKILL_FILE_NAME2) {
|
|
3009
|
+
try {
|
|
3010
|
+
content = injectSourceMetadata({
|
|
3011
|
+
content,
|
|
3012
|
+
source: sourceUrl,
|
|
3013
|
+
repository,
|
|
3014
|
+
ref: provenanceRef
|
|
3015
|
+
});
|
|
3016
|
+
} catch {
|
|
3017
|
+
logger5.warn(
|
|
3018
|
+
`Frontmatter in ${file.path} (${sourceKey}) is invalid. Prepending a fresh provenance block.`
|
|
3019
|
+
);
|
|
3020
|
+
content = `---
|
|
3021
|
+
source: ${sourceUrl}
|
|
3022
|
+
repository: ${repository}
|
|
3023
|
+
ref: ${provenanceRef}
|
|
3024
|
+
---
|
|
3025
|
+
${content}`;
|
|
3026
|
+
}
|
|
3027
|
+
}
|
|
3028
|
+
const absolutePath = join9(scopeRoot, deployRelative);
|
|
3029
|
+
deployed.push({ relativeToScopeRoot: deployRelative, absolutePath, content });
|
|
3030
|
+
if (!frozen) {
|
|
3031
|
+
await writeFileContent(absolutePath, content);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
deployed.sort(
|
|
3035
|
+
(a, b) => a.relativeToScopeRoot < b.relativeToScopeRoot ? -1 : a.relativeToScopeRoot > b.relativeToScopeRoot ? 1 : 0
|
|
3036
|
+
);
|
|
3037
|
+
const deployedFiles = deployed.map((d) => d.relativeToScopeRoot);
|
|
3038
|
+
const contentHash = computeContentHash2(deployed);
|
|
3039
|
+
if (frozen && locked?.content_hash) {
|
|
3040
|
+
if (RULESYNC_CONTENT_HASH_REGEX2.test(locked.content_hash)) {
|
|
3041
|
+
if (locked.content_hash !== contentHash) {
|
|
3042
|
+
throw new Error(
|
|
3043
|
+
`content_hash mismatch for ${sourceKey} skill "${sk.name}" (agent=${agent}, scope=${scope}): lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
|
|
3044
|
+
);
|
|
3045
|
+
}
|
|
3046
|
+
} else {
|
|
3047
|
+
logger5.debug(
|
|
3048
|
+
`Skipping content_hash integrity check for ${sourceKey} skill "${sk.name}": recorded hash "${locked.content_hash}" was not written by rulesync.`
|
|
3049
|
+
);
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
const installation = {
|
|
3053
|
+
source: sourceKey,
|
|
3054
|
+
owner,
|
|
3055
|
+
repo,
|
|
3056
|
+
agent,
|
|
3057
|
+
scope,
|
|
3058
|
+
skill: sk.name,
|
|
3059
|
+
resolved_ref: resolvedRef,
|
|
3060
|
+
resolved_commit: resolvedSha,
|
|
3061
|
+
install_dir: toPosixPath(installRelDir),
|
|
3062
|
+
deployed_files: deployedFiles,
|
|
3063
|
+
content_hash: contentHash
|
|
3064
|
+
};
|
|
3065
|
+
if (rs.ref !== void 0) {
|
|
3066
|
+
installation.requested_ref = rs.ref;
|
|
3067
|
+
}
|
|
3068
|
+
results.push({ installation, deployed });
|
|
3069
|
+
logger5.info(
|
|
3070
|
+
`Installed gh skill "${sk.name}" from ${sourceKey} (agent=${agent}, scope=${scope}, ref=${resolvedRef})`
|
|
3071
|
+
);
|
|
3072
|
+
}
|
|
3073
|
+
return results;
|
|
3074
|
+
}
|
|
3075
|
+
async function removeStaleFile(params) {
|
|
3076
|
+
const { relativePath, scope, baseDir, logger: logger5 } = params;
|
|
3077
|
+
if (posix3.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
|
|
3078
|
+
logger5.warn(`Refusing to remove stale gh file with suspicious path: "${relativePath}".`);
|
|
3079
|
+
return;
|
|
3080
|
+
}
|
|
3081
|
+
const scopeRoot = scope === "user" ? getHomeDirectory() : baseDir;
|
|
3082
|
+
try {
|
|
3083
|
+
checkPathTraversal({ relativePath, intendedRootDir: scopeRoot });
|
|
3084
|
+
} catch {
|
|
3085
|
+
logger5.warn(`Refusing to remove stale gh file outside ${scope} root: "${relativePath}".`);
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
const absolute = join9(scopeRoot, relativePath);
|
|
3089
|
+
await removeFile(absolute);
|
|
3090
|
+
logger5.debug(`Removed stale gh file: ${relativePath}`);
|
|
3091
|
+
}
|
|
3092
|
+
function is404(error) {
|
|
3093
|
+
if (error instanceof GitHubClientError && error.statusCode === 404) {
|
|
3094
|
+
return true;
|
|
3095
|
+
}
|
|
3096
|
+
if (typeof error === "object" && error !== null && "statusCode" in error && error.statusCode === 404) {
|
|
3097
|
+
return true;
|
|
3098
|
+
}
|
|
3099
|
+
return false;
|
|
3100
|
+
}
|
|
3101
|
+
function computeContentHash2(files) {
|
|
3102
|
+
const hash = createHash2("sha256");
|
|
3103
|
+
for (const { relativeToScopeRoot, content } of files) {
|
|
3104
|
+
hash.update(relativeToScopeRoot);
|
|
3105
|
+
hash.update("\0");
|
|
3106
|
+
hash.update(content);
|
|
3107
|
+
hash.update("\0");
|
|
3108
|
+
}
|
|
3109
|
+
return `sha256:${hash.digest("hex")}`;
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
// src/lib/sources.ts
|
|
3113
|
+
import { join as join12, resolve, sep } from "path";
|
|
3114
|
+
import { Semaphore as Semaphore4 } from "es-toolkit/promise";
|
|
3115
|
+
|
|
1955
3116
|
// src/lib/git-client.ts
|
|
1956
3117
|
import { execFile } from "child_process";
|
|
1957
|
-
import { isAbsolute, join as
|
|
3118
|
+
import { isAbsolute, join as join10, relative } from "path";
|
|
1958
3119
|
import { promisify } from "util";
|
|
1959
3120
|
var execFileAsync = promisify(execFile);
|
|
1960
3121
|
var GIT_TIMEOUT_MS = 6e4;
|
|
@@ -2076,7 +3237,7 @@ async function fetchSkillFiles(params) {
|
|
|
2076
3237
|
timeout: GIT_TIMEOUT_MS
|
|
2077
3238
|
});
|
|
2078
3239
|
await execFileAsync("git", ["-C", tmpDir, "checkout"], { timeout: GIT_TIMEOUT_MS });
|
|
2079
|
-
const skillsDir =
|
|
3240
|
+
const skillsDir = join10(tmpDir, skillsPath);
|
|
2080
3241
|
if (!await directoryExists(skillsDir)) return [];
|
|
2081
3242
|
return await walkDirectory(skillsDir, skillsDir, 0, { totalFiles: 0, totalSize: 0 }, logger5);
|
|
2082
3243
|
} catch (error) {
|
|
@@ -2098,7 +3259,7 @@ async function walkDirectory(dir, baseDir, depth = 0, ctx = { totalFiles: 0, tot
|
|
|
2098
3259
|
const results = [];
|
|
2099
3260
|
for (const name of await listDirectoryFiles(dir)) {
|
|
2100
3261
|
if (name === ".git") continue;
|
|
2101
|
-
const fullPath =
|
|
3262
|
+
const fullPath = join10(dir, name);
|
|
2102
3263
|
if (await isSymlink(fullPath)) {
|
|
2103
3264
|
logger5?.warn(`Skipping symlink "${fullPath}".`);
|
|
2104
3265
|
continue;
|
|
@@ -2133,29 +3294,29 @@ async function walkDirectory(dir, baseDir, depth = 0, ctx = { totalFiles: 0, tot
|
|
|
2133
3294
|
}
|
|
2134
3295
|
|
|
2135
3296
|
// src/lib/sources-lock.ts
|
|
2136
|
-
import { createHash } from "crypto";
|
|
2137
|
-
import { join as
|
|
2138
|
-
import { optional, refine, z as
|
|
3297
|
+
import { createHash as createHash3 } from "crypto";
|
|
3298
|
+
import { join as join11 } from "path";
|
|
3299
|
+
import { optional as optional4, refine as refine3, z as z7 } from "zod/mini";
|
|
2139
3300
|
var LOCKFILE_VERSION = 1;
|
|
2140
|
-
var LockedSkillSchema =
|
|
2141
|
-
integrity:
|
|
3301
|
+
var LockedSkillSchema = z7.object({
|
|
3302
|
+
integrity: z7.string()
|
|
2142
3303
|
});
|
|
2143
|
-
var LockedSourceSchema =
|
|
2144
|
-
requestedRef:
|
|
2145
|
-
resolvedRef:
|
|
2146
|
-
resolvedAt:
|
|
2147
|
-
skills:
|
|
3304
|
+
var LockedSourceSchema = z7.object({
|
|
3305
|
+
requestedRef: optional4(z7.string()),
|
|
3306
|
+
resolvedRef: z7.string().check(refine3((v) => /^[0-9a-f]{40}$/.test(v), "resolvedRef must be a 40-character hex SHA")),
|
|
3307
|
+
resolvedAt: optional4(z7.string()),
|
|
3308
|
+
skills: z7.record(z7.string(), LockedSkillSchema)
|
|
2148
3309
|
});
|
|
2149
|
-
var SourcesLockSchema =
|
|
2150
|
-
lockfileVersion:
|
|
2151
|
-
sources:
|
|
3310
|
+
var SourcesLockSchema = z7.object({
|
|
3311
|
+
lockfileVersion: z7.number(),
|
|
3312
|
+
sources: z7.record(z7.string(), LockedSourceSchema)
|
|
2152
3313
|
});
|
|
2153
|
-
var LegacyLockedSourceSchema =
|
|
2154
|
-
resolvedRef:
|
|
2155
|
-
skills:
|
|
3314
|
+
var LegacyLockedSourceSchema = z7.object({
|
|
3315
|
+
resolvedRef: z7.string(),
|
|
3316
|
+
skills: z7.array(z7.string())
|
|
2156
3317
|
});
|
|
2157
|
-
var LegacySourcesLockSchema =
|
|
2158
|
-
sources:
|
|
3318
|
+
var LegacySourcesLockSchema = z7.object({
|
|
3319
|
+
sources: z7.record(z7.string(), LegacyLockedSourceSchema)
|
|
2159
3320
|
});
|
|
2160
3321
|
function migrateLegacyLock(params) {
|
|
2161
3322
|
const { legacy, logger: logger5 } = params;
|
|
@@ -2180,7 +3341,7 @@ function createEmptyLock() {
|
|
|
2180
3341
|
}
|
|
2181
3342
|
async function readLockFile(params) {
|
|
2182
3343
|
const { logger: logger5 } = params;
|
|
2183
|
-
const lockPath =
|
|
3344
|
+
const lockPath = join11(params.baseDir, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH);
|
|
2184
3345
|
if (!await fileExists(lockPath)) {
|
|
2185
3346
|
logger5.debug("No sources lockfile found, starting fresh.");
|
|
2186
3347
|
return createEmptyLock();
|
|
@@ -2209,13 +3370,13 @@ async function readLockFile(params) {
|
|
|
2209
3370
|
}
|
|
2210
3371
|
async function writeLockFile(params) {
|
|
2211
3372
|
const { logger: logger5 } = params;
|
|
2212
|
-
const lockPath =
|
|
3373
|
+
const lockPath = join11(params.baseDir, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH);
|
|
2213
3374
|
const content = JSON.stringify(params.lock, null, 2) + "\n";
|
|
2214
3375
|
await writeFileContent(lockPath, content);
|
|
2215
3376
|
logger5.debug(`Wrote sources lockfile to ${lockPath}`);
|
|
2216
3377
|
}
|
|
2217
3378
|
function computeSkillIntegrity(files) {
|
|
2218
|
-
const hash =
|
|
3379
|
+
const hash = createHash3("sha256");
|
|
2219
3380
|
const sorted = files.toSorted((a, b) => a.path.localeCompare(b.path));
|
|
2220
3381
|
for (const file of sorted) {
|
|
2221
3382
|
hash.update(file.path);
|
|
@@ -2383,7 +3544,7 @@ function logGitClientHints(params) {
|
|
|
2383
3544
|
async function checkLockedSkillsExist(curatedDir, skillNames) {
|
|
2384
3545
|
if (skillNames.length === 0) return true;
|
|
2385
3546
|
for (const name of skillNames) {
|
|
2386
|
-
if (!await directoryExists(
|
|
3547
|
+
if (!await directoryExists(join12(curatedDir, name))) {
|
|
2387
3548
|
return false;
|
|
2388
3549
|
}
|
|
2389
3550
|
}
|
|
@@ -2393,7 +3554,7 @@ async function cleanPreviousCuratedSkills(params) {
|
|
|
2393
3554
|
const { curatedDir, lockedSkillNames, logger: logger5 } = params;
|
|
2394
3555
|
const resolvedCuratedDir = resolve(curatedDir);
|
|
2395
3556
|
for (const prevSkill of lockedSkillNames) {
|
|
2396
|
-
const prevDir =
|
|
3557
|
+
const prevDir = join12(curatedDir, prevSkill);
|
|
2397
3558
|
if (!resolve(prevDir).startsWith(resolvedCuratedDir + sep)) {
|
|
2398
3559
|
logger5.warn(
|
|
2399
3560
|
`Skipping removal of "${prevSkill}": resolved path is outside the curated directory.`
|
|
@@ -2433,9 +3594,9 @@ async function writeSkillAndComputeIntegrity(params) {
|
|
|
2433
3594
|
for (const file of files) {
|
|
2434
3595
|
checkPathTraversal({
|
|
2435
3596
|
relativePath: file.relativePath,
|
|
2436
|
-
intendedRootDir:
|
|
3597
|
+
intendedRootDir: join12(curatedDir, skillName)
|
|
2437
3598
|
});
|
|
2438
|
-
await writeFileContent(
|
|
3599
|
+
await writeFileContent(join12(curatedDir, skillName, file.relativePath), file.content);
|
|
2439
3600
|
written.push({ path: file.relativePath, content: file.content });
|
|
2440
3601
|
}
|
|
2441
3602
|
const integrity = computeSkillIntegrity(written);
|
|
@@ -2479,6 +3640,40 @@ function buildLockUpdate(params) {
|
|
|
2479
3640
|
);
|
|
2480
3641
|
return { updatedLock, fetchedNames };
|
|
2481
3642
|
}
|
|
3643
|
+
function getFirstPathSeparatorIndex(path2) {
|
|
3644
|
+
const slashIndex = path2.indexOf("/");
|
|
3645
|
+
const backslashIndex = path2.indexOf("\\");
|
|
3646
|
+
if (slashIndex === -1) return backslashIndex;
|
|
3647
|
+
if (backslashIndex === -1) return slashIndex;
|
|
3648
|
+
return Math.min(slashIndex, backslashIndex);
|
|
3649
|
+
}
|
|
3650
|
+
function groupRemoteFilesBySkillRoot(params) {
|
|
3651
|
+
const { remoteFiles, skillFilter, isWildcard } = params;
|
|
3652
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3653
|
+
const rootLevelFiles = [];
|
|
3654
|
+
for (const file of remoteFiles) {
|
|
3655
|
+
const separatorIndex = getFirstPathSeparatorIndex(file.relativePath);
|
|
3656
|
+
if (separatorIndex === -1) {
|
|
3657
|
+
rootLevelFiles.push(file);
|
|
3658
|
+
continue;
|
|
3659
|
+
}
|
|
3660
|
+
const skillName = file.relativePath.substring(0, separatorIndex);
|
|
3661
|
+
if (skillName.length === 0) {
|
|
3662
|
+
continue;
|
|
3663
|
+
}
|
|
3664
|
+
const innerPath = file.relativePath.substring(separatorIndex + 1);
|
|
3665
|
+
const groupedFiles = grouped.get(skillName) ?? [];
|
|
3666
|
+
groupedFiles.push({ relativePath: innerPath, content: file.content });
|
|
3667
|
+
grouped.set(skillName, groupedFiles);
|
|
3668
|
+
}
|
|
3669
|
+
if (grouped.size === 0 && !isWildcard && skillFilter.length === 1) {
|
|
3670
|
+
const [singleSkillName] = skillFilter;
|
|
3671
|
+
if (singleSkillName !== void 0 && rootLevelFiles.length > 0) {
|
|
3672
|
+
grouped.set(singleSkillName, rootLevelFiles);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
return grouped;
|
|
3676
|
+
}
|
|
2482
3677
|
async function fetchSource(params) {
|
|
2483
3678
|
const {
|
|
2484
3679
|
sourceEntry,
|
|
@@ -2512,7 +3707,7 @@ async function fetchSource(params) {
|
|
|
2512
3707
|
ref = resolvedSha;
|
|
2513
3708
|
logger5.debug(`Resolved ${sourceKey} ref "${requestedRef}" to SHA: ${resolvedSha}`);
|
|
2514
3709
|
}
|
|
2515
|
-
const curatedDir =
|
|
3710
|
+
const curatedDir = join12(baseDir, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
|
|
2516
3711
|
if (locked && resolvedSha === locked.resolvedRef && !updateSources) {
|
|
2517
3712
|
const allExist = await checkLockedSkillsExist(curatedDir, lockedSkillNames);
|
|
2518
3713
|
if (allExist) {
|
|
@@ -2526,11 +3721,60 @@ async function fetchSource(params) {
|
|
|
2526
3721
|
}
|
|
2527
3722
|
const skillFilter = sourceEntry.skills ?? ["*"];
|
|
2528
3723
|
const isWildcard = skillFilter.length === 1 && skillFilter[0] === "*";
|
|
3724
|
+
const semaphore = new Semaphore4(FETCH_CONCURRENCY_LIMIT);
|
|
3725
|
+
const fetchedSkills = {};
|
|
2529
3726
|
const skillsBasePath = parsed.path ?? "skills";
|
|
2530
3727
|
let remoteSkillDirs;
|
|
3728
|
+
let remoteSkillNames = [];
|
|
3729
|
+
let fallbackHandled = false;
|
|
2531
3730
|
try {
|
|
2532
3731
|
const entries = await client.listDirectory(parsed.owner, parsed.repo, skillsBasePath, ref);
|
|
2533
3732
|
remoteSkillDirs = entries.filter((e) => e.type === "dir").map((e) => ({ name: e.name, path: e.path }));
|
|
3733
|
+
if (remoteSkillDirs.length === 0 && !isWildcard && skillFilter.length === 1) {
|
|
3734
|
+
const rootFiles = entries.filter((entry) => entry.type === "file");
|
|
3735
|
+
const rootSkillFiles = [];
|
|
3736
|
+
for (const file of rootFiles) {
|
|
3737
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
3738
|
+
logger5.warn(
|
|
3739
|
+
`Skipping file "${file.path}" (${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit).`
|
|
3740
|
+
);
|
|
3741
|
+
continue;
|
|
3742
|
+
}
|
|
3743
|
+
const content = await withSemaphore(
|
|
3744
|
+
semaphore,
|
|
3745
|
+
() => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
|
|
3746
|
+
);
|
|
3747
|
+
rootSkillFiles.push({ relativePath: file.name, content });
|
|
3748
|
+
}
|
|
3749
|
+
const groupedRootFiles = groupRemoteFilesBySkillRoot({
|
|
3750
|
+
remoteFiles: rootSkillFiles,
|
|
3751
|
+
skillFilter,
|
|
3752
|
+
isWildcard
|
|
3753
|
+
});
|
|
3754
|
+
const [fallbackSkillName] = groupedRootFiles.keys();
|
|
3755
|
+
if (fallbackSkillName !== void 0) {
|
|
3756
|
+
fallbackHandled = true;
|
|
3757
|
+
remoteSkillNames = [fallbackSkillName];
|
|
3758
|
+
if (!shouldSkipSkill({
|
|
3759
|
+
skillName: fallbackSkillName,
|
|
3760
|
+
sourceKey,
|
|
3761
|
+
localSkillNames,
|
|
3762
|
+
alreadyFetchedSkillNames,
|
|
3763
|
+
logger: logger5
|
|
3764
|
+
})) {
|
|
3765
|
+
fetchedSkills[fallbackSkillName] = await writeSkillAndComputeIntegrity({
|
|
3766
|
+
skillName: fallbackSkillName,
|
|
3767
|
+
files: groupedRootFiles.get(fallbackSkillName) ?? [],
|
|
3768
|
+
curatedDir,
|
|
3769
|
+
locked,
|
|
3770
|
+
resolvedSha,
|
|
3771
|
+
sourceKey,
|
|
3772
|
+
logger: logger5
|
|
3773
|
+
});
|
|
3774
|
+
logger5.debug(`Fetched skill "${fallbackSkillName}" from ${sourceKey}`);
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
2534
3778
|
} catch (error) {
|
|
2535
3779
|
if (error instanceof GitHubClientError && error.statusCode === 404) {
|
|
2536
3780
|
logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
|
|
@@ -2539,8 +3783,9 @@ async function fetchSource(params) {
|
|
|
2539
3783
|
throw error;
|
|
2540
3784
|
}
|
|
2541
3785
|
const filteredDirs = isWildcard ? remoteSkillDirs : remoteSkillDirs.filter((d) => skillFilter.includes(d.name));
|
|
2542
|
-
|
|
2543
|
-
|
|
3786
|
+
if (!fallbackHandled) {
|
|
3787
|
+
remoteSkillNames = filteredDirs.map((d) => d.name);
|
|
3788
|
+
}
|
|
2544
3789
|
if (locked) {
|
|
2545
3790
|
await cleanPreviousCuratedSkills({ curatedDir, lockedSkillNames, logger: logger5 });
|
|
2546
3791
|
}
|
|
@@ -2598,7 +3843,7 @@ async function fetchSource(params) {
|
|
|
2598
3843
|
locked,
|
|
2599
3844
|
requestedRef,
|
|
2600
3845
|
resolvedSha,
|
|
2601
|
-
remoteSkillNames
|
|
3846
|
+
remoteSkillNames,
|
|
2602
3847
|
logger: logger5
|
|
2603
3848
|
});
|
|
2604
3849
|
return {
|
|
@@ -2637,7 +3882,7 @@ async function fetchSourceViaGit(params) {
|
|
|
2637
3882
|
requestedRef = def.ref;
|
|
2638
3883
|
resolvedSha = def.sha;
|
|
2639
3884
|
}
|
|
2640
|
-
const curatedDir =
|
|
3885
|
+
const curatedDir = join12(baseDir, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
|
|
2641
3886
|
if (locked && resolvedSha === locked.resolvedRef && !updateSources) {
|
|
2642
3887
|
if (await checkLockedSkillsExist(curatedDir, lockedSkillNames)) {
|
|
2643
3888
|
return { skillCount: 0, fetchedSkillNames: lockedSkillNames, updatedLock: lock };
|
|
@@ -2660,28 +3905,7 @@ async function fetchSourceViaGit(params) {
|
|
|
2660
3905
|
ref: requestedRef,
|
|
2661
3906
|
skillsPath: sourceEntry.path ?? "skills"
|
|
2662
3907
|
});
|
|
2663
|
-
const skillFileMap =
|
|
2664
|
-
for (const file of remoteFiles) {
|
|
2665
|
-
const idx = file.relativePath.indexOf("/");
|
|
2666
|
-
if (idx === -1) continue;
|
|
2667
|
-
const name = file.relativePath.substring(0, idx);
|
|
2668
|
-
const inner = file.relativePath.substring(idx + 1);
|
|
2669
|
-
const arr = skillFileMap.get(name) ?? [];
|
|
2670
|
-
arr.push({ relativePath: inner, content: file.content });
|
|
2671
|
-
skillFileMap.set(name, arr);
|
|
2672
|
-
}
|
|
2673
|
-
if (skillFileMap.size === 0 && !isWildcard && skillFilter.length === 1) {
|
|
2674
|
-
const [singleSkillName] = skillFilter;
|
|
2675
|
-
if (singleSkillName !== void 0) {
|
|
2676
|
-
const rootFiles = remoteFiles.filter((f) => f.relativePath.indexOf("/") === -1);
|
|
2677
|
-
if (rootFiles.length > 0) {
|
|
2678
|
-
skillFileMap.set(
|
|
2679
|
-
singleSkillName,
|
|
2680
|
-
rootFiles.map((f) => ({ relativePath: f.relativePath, content: f.content }))
|
|
2681
|
-
);
|
|
2682
|
-
}
|
|
2683
|
-
}
|
|
2684
|
-
}
|
|
3908
|
+
const skillFileMap = groupRemoteFilesBySkillRoot({ remoteFiles, skillFilter, isWildcard });
|
|
2685
3909
|
const allNames = [...skillFileMap.keys()];
|
|
2686
3910
|
const filteredNames = isWildcard ? allNames : allNames.filter((n) => skillFilter.includes(n));
|
|
2687
3911
|
if (locked) {
|
|
@@ -2726,21 +3950,47 @@ async function fetchSourceViaGit(params) {
|
|
|
2726
3950
|
}
|
|
2727
3951
|
|
|
2728
3952
|
// src/cli/commands/install.ts
|
|
3953
|
+
var INSTALL_MODES = ["rulesync", "apm", "gh"];
|
|
2729
3954
|
async function installCommand(logger5, options) {
|
|
3955
|
+
const mode = options.mode ?? "rulesync";
|
|
3956
|
+
if (mode === "gh") {
|
|
3957
|
+
await runGhInstall(logger5, options);
|
|
3958
|
+
return;
|
|
3959
|
+
}
|
|
3960
|
+
if (mode === "apm") {
|
|
3961
|
+
await runApmInstall(logger5, options);
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
await runRulesyncInstall(logger5, options);
|
|
3965
|
+
}
|
|
3966
|
+
async function runRulesyncInstall(logger5, options) {
|
|
3967
|
+
const baseDir = process.cwd();
|
|
3968
|
+
const apmExists = await apmManifestExists(baseDir);
|
|
2730
3969
|
const config = await ConfigResolver.resolve({
|
|
2731
3970
|
configPath: options.configPath,
|
|
2732
3971
|
verbose: options.verbose,
|
|
2733
3972
|
silent: options.silent
|
|
2734
3973
|
});
|
|
2735
3974
|
const sources = config.getSources();
|
|
3975
|
+
if (apmExists && sources.length > 0) {
|
|
3976
|
+
throw new Error(
|
|
3977
|
+
"Both apm.yml and rulesync.jsonc `sources` are defined. Pass --mode apm or --mode rulesync to disambiguate."
|
|
3978
|
+
);
|
|
3979
|
+
}
|
|
2736
3980
|
if (sources.length === 0) {
|
|
3981
|
+
if (apmExists) {
|
|
3982
|
+
logger5.warn(
|
|
3983
|
+
"No sources defined in rulesync.jsonc, but apm.yml is present. Did you mean --mode apm?"
|
|
3984
|
+
);
|
|
3985
|
+
return;
|
|
3986
|
+
}
|
|
2737
3987
|
logger5.warn("No sources defined in configuration. Nothing to install.");
|
|
2738
3988
|
return;
|
|
2739
3989
|
}
|
|
2740
3990
|
logger5.debug(`Installing skills from ${sources.length} source(s)...`);
|
|
2741
3991
|
const result = await resolveAndFetchSources({
|
|
2742
3992
|
sources,
|
|
2743
|
-
baseDir
|
|
3993
|
+
baseDir,
|
|
2744
3994
|
options: {
|
|
2745
3995
|
updateSources: options.update,
|
|
2746
3996
|
frozen: options.frozen,
|
|
@@ -2760,21 +4010,95 @@ async function installCommand(logger5, options) {
|
|
|
2760
4010
|
logger5.success(`All skills up to date (${result.sourcesProcessed} source(s) checked).`);
|
|
2761
4011
|
}
|
|
2762
4012
|
}
|
|
4013
|
+
async function runApmInstall(logger5, options) {
|
|
4014
|
+
const baseDir = process.cwd();
|
|
4015
|
+
if (!await apmManifestExists(baseDir)) {
|
|
4016
|
+
throw new Error(
|
|
4017
|
+
"--mode apm requires an apm.yml at the project root. Create one or drop --mode apm to fall back to rulesync mode."
|
|
4018
|
+
);
|
|
4019
|
+
}
|
|
4020
|
+
const result = await installApm({
|
|
4021
|
+
baseDir,
|
|
4022
|
+
options: {
|
|
4023
|
+
update: options.update,
|
|
4024
|
+
frozen: options.frozen,
|
|
4025
|
+
token: options.token
|
|
4026
|
+
},
|
|
4027
|
+
logger: logger5
|
|
4028
|
+
});
|
|
4029
|
+
if (logger5.jsonMode) {
|
|
4030
|
+
logger5.captureData("dependenciesProcessed", result.dependenciesProcessed);
|
|
4031
|
+
logger5.captureData("deployedFileCount", result.deployedFileCount);
|
|
4032
|
+
logger5.captureData("failedDependencyCount", result.failedDependencyCount);
|
|
4033
|
+
}
|
|
4034
|
+
if (result.failedDependencyCount > 0) {
|
|
4035
|
+
throw new Error(
|
|
4036
|
+
`Failed to install ${result.failedDependencyCount} of ${result.dependenciesProcessed} apm dependency(ies). See the log above for details.`
|
|
4037
|
+
);
|
|
4038
|
+
}
|
|
4039
|
+
if (result.deployedFileCount > 0) {
|
|
4040
|
+
logger5.success(
|
|
4041
|
+
`Installed ${result.deployedFileCount} file(s) from ${result.dependenciesProcessed} apm dependency(ies).`
|
|
4042
|
+
);
|
|
4043
|
+
} else {
|
|
4044
|
+
logger5.success(`All apm dependencies up to date (${result.dependenciesProcessed} checked).`);
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
async function runGhInstall(logger5, options) {
|
|
4048
|
+
const baseDir = process.cwd();
|
|
4049
|
+
const config = await ConfigResolver.resolve({
|
|
4050
|
+
configPath: options.configPath,
|
|
4051
|
+
verbose: options.verbose,
|
|
4052
|
+
silent: options.silent
|
|
4053
|
+
});
|
|
4054
|
+
const sources = config.getSources();
|
|
4055
|
+
if (sources.length === 0) {
|
|
4056
|
+
logger5.warn("No sources defined in configuration. Nothing to install.");
|
|
4057
|
+
return;
|
|
4058
|
+
}
|
|
4059
|
+
const result = await installGh({
|
|
4060
|
+
baseDir,
|
|
4061
|
+
sources,
|
|
4062
|
+
options: {
|
|
4063
|
+
update: options.update,
|
|
4064
|
+
frozen: options.frozen,
|
|
4065
|
+
token: options.token
|
|
4066
|
+
},
|
|
4067
|
+
logger: logger5
|
|
4068
|
+
});
|
|
4069
|
+
if (logger5.jsonMode) {
|
|
4070
|
+
logger5.captureData("sourcesProcessed", result.sourcesProcessed);
|
|
4071
|
+
logger5.captureData("installedSkillCount", result.installedSkillCount);
|
|
4072
|
+
logger5.captureData("failedSourceCount", result.failedSourceCount);
|
|
4073
|
+
}
|
|
4074
|
+
if (result.failedSourceCount > 0) {
|
|
4075
|
+
throw new Error(
|
|
4076
|
+
`Failed to install ${result.failedSourceCount} of ${result.sourcesProcessed} gh source(s). See the log above for details.`
|
|
4077
|
+
);
|
|
4078
|
+
}
|
|
4079
|
+
if (result.installedSkillCount > 0) {
|
|
4080
|
+
logger5.success(
|
|
4081
|
+
`Installed ${result.installedSkillCount} skill(s) from ${result.sourcesProcessed} gh source(s).`
|
|
4082
|
+
);
|
|
4083
|
+
} else {
|
|
4084
|
+
logger5.success(`All gh sources up to date (${result.sourcesProcessed} checked).`);
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
2763
4087
|
|
|
2764
4088
|
// src/cli/commands/mcp.ts
|
|
2765
4089
|
import { FastMCP } from "fastmcp";
|
|
2766
4090
|
|
|
2767
4091
|
// src/mcp/tools.ts
|
|
2768
|
-
import { z as
|
|
4092
|
+
import { z as z18 } from "zod/mini";
|
|
2769
4093
|
|
|
2770
4094
|
// src/mcp/commands.ts
|
|
2771
|
-
import { basename, join as
|
|
2772
|
-
import { z as
|
|
4095
|
+
import { basename as basename2, join as join13 } from "path";
|
|
4096
|
+
import { z as z8 } from "zod/mini";
|
|
2773
4097
|
var logger = new ConsoleLogger({ verbose: false, silent: true });
|
|
2774
4098
|
var maxCommandSizeBytes = 1024 * 1024;
|
|
2775
4099
|
var maxCommandsCount = 1e3;
|
|
2776
4100
|
async function listCommands() {
|
|
2777
|
-
const commandsDir =
|
|
4101
|
+
const commandsDir = join13(process.cwd(), RULESYNC_COMMANDS_RELATIVE_DIR_PATH);
|
|
2778
4102
|
try {
|
|
2779
4103
|
const files = await listDirectoryFiles(commandsDir);
|
|
2780
4104
|
const mdFiles = files.filter((file) => file.endsWith(".md"));
|
|
@@ -2790,7 +4114,7 @@ async function listCommands() {
|
|
|
2790
4114
|
});
|
|
2791
4115
|
const frontmatter = command.getFrontmatter();
|
|
2792
4116
|
return {
|
|
2793
|
-
relativePathFromCwd:
|
|
4117
|
+
relativePathFromCwd: join13(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, file),
|
|
2794
4118
|
frontmatter
|
|
2795
4119
|
};
|
|
2796
4120
|
} catch (error) {
|
|
@@ -2812,13 +4136,13 @@ async function getCommand({ relativePathFromCwd }) {
|
|
|
2812
4136
|
relativePath: relativePathFromCwd,
|
|
2813
4137
|
intendedRootDir: process.cwd()
|
|
2814
4138
|
});
|
|
2815
|
-
const filename =
|
|
4139
|
+
const filename = basename2(relativePathFromCwd);
|
|
2816
4140
|
try {
|
|
2817
4141
|
const command = await RulesyncCommand.fromFile({
|
|
2818
4142
|
relativeFilePath: filename
|
|
2819
4143
|
});
|
|
2820
4144
|
return {
|
|
2821
|
-
relativePathFromCwd:
|
|
4145
|
+
relativePathFromCwd: join13(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename),
|
|
2822
4146
|
frontmatter: command.getFrontmatter(),
|
|
2823
4147
|
body: command.getBody()
|
|
2824
4148
|
};
|
|
@@ -2837,7 +4161,7 @@ async function putCommand({
|
|
|
2837
4161
|
relativePath: relativePathFromCwd,
|
|
2838
4162
|
intendedRootDir: process.cwd()
|
|
2839
4163
|
});
|
|
2840
|
-
const filename =
|
|
4164
|
+
const filename = basename2(relativePathFromCwd);
|
|
2841
4165
|
const estimatedSize = JSON.stringify(frontmatter).length + body.length;
|
|
2842
4166
|
if (estimatedSize > maxCommandSizeBytes) {
|
|
2843
4167
|
throw new Error(
|
|
@@ -2847,7 +4171,7 @@ async function putCommand({
|
|
|
2847
4171
|
try {
|
|
2848
4172
|
const existingCommands = await listCommands();
|
|
2849
4173
|
const isUpdate = existingCommands.some(
|
|
2850
|
-
(command2) => command2.relativePathFromCwd ===
|
|
4174
|
+
(command2) => command2.relativePathFromCwd === join13(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename)
|
|
2851
4175
|
);
|
|
2852
4176
|
if (!isUpdate && existingCommands.length >= maxCommandsCount) {
|
|
2853
4177
|
throw new Error(
|
|
@@ -2864,11 +4188,11 @@ async function putCommand({
|
|
|
2864
4188
|
fileContent,
|
|
2865
4189
|
validate: true
|
|
2866
4190
|
});
|
|
2867
|
-
const commandsDir =
|
|
4191
|
+
const commandsDir = join13(process.cwd(), RULESYNC_COMMANDS_RELATIVE_DIR_PATH);
|
|
2868
4192
|
await ensureDir(commandsDir);
|
|
2869
4193
|
await writeFileContent(command.getFilePath(), command.getFileContent());
|
|
2870
4194
|
return {
|
|
2871
|
-
relativePathFromCwd:
|
|
4195
|
+
relativePathFromCwd: join13(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename),
|
|
2872
4196
|
frontmatter: command.getFrontmatter(),
|
|
2873
4197
|
body: command.getBody()
|
|
2874
4198
|
};
|
|
@@ -2883,12 +4207,12 @@ async function deleteCommand({ relativePathFromCwd }) {
|
|
|
2883
4207
|
relativePath: relativePathFromCwd,
|
|
2884
4208
|
intendedRootDir: process.cwd()
|
|
2885
4209
|
});
|
|
2886
|
-
const filename =
|
|
2887
|
-
const fullPath =
|
|
4210
|
+
const filename = basename2(relativePathFromCwd);
|
|
4211
|
+
const fullPath = join13(process.cwd(), RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename);
|
|
2888
4212
|
try {
|
|
2889
4213
|
await removeFile(fullPath);
|
|
2890
4214
|
return {
|
|
2891
|
-
relativePathFromCwd:
|
|
4215
|
+
relativePathFromCwd: join13(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename)
|
|
2892
4216
|
};
|
|
2893
4217
|
} catch (error) {
|
|
2894
4218
|
throw new Error(`Failed to delete command file ${relativePathFromCwd}: ${formatError(error)}`, {
|
|
@@ -2897,23 +4221,23 @@ async function deleteCommand({ relativePathFromCwd }) {
|
|
|
2897
4221
|
}
|
|
2898
4222
|
}
|
|
2899
4223
|
var commandToolSchemas = {
|
|
2900
|
-
listCommands:
|
|
2901
|
-
getCommand:
|
|
2902
|
-
relativePathFromCwd:
|
|
4224
|
+
listCommands: z8.object({}),
|
|
4225
|
+
getCommand: z8.object({
|
|
4226
|
+
relativePathFromCwd: z8.string()
|
|
2903
4227
|
}),
|
|
2904
|
-
putCommand:
|
|
2905
|
-
relativePathFromCwd:
|
|
4228
|
+
putCommand: z8.object({
|
|
4229
|
+
relativePathFromCwd: z8.string(),
|
|
2906
4230
|
frontmatter: RulesyncCommandFrontmatterSchema,
|
|
2907
|
-
body:
|
|
4231
|
+
body: z8.string()
|
|
2908
4232
|
}),
|
|
2909
|
-
deleteCommand:
|
|
2910
|
-
relativePathFromCwd:
|
|
4233
|
+
deleteCommand: z8.object({
|
|
4234
|
+
relativePathFromCwd: z8.string()
|
|
2911
4235
|
})
|
|
2912
4236
|
};
|
|
2913
4237
|
var commandTools = {
|
|
2914
4238
|
listCommands: {
|
|
2915
4239
|
name: "listCommands",
|
|
2916
|
-
description: `List all commands from ${
|
|
4240
|
+
description: `List all commands from ${join13(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
|
|
2917
4241
|
parameters: commandToolSchemas.listCommands,
|
|
2918
4242
|
execute: async () => {
|
|
2919
4243
|
const commands = await listCommands();
|
|
@@ -2955,15 +4279,15 @@ var commandTools = {
|
|
|
2955
4279
|
};
|
|
2956
4280
|
|
|
2957
4281
|
// src/mcp/generate.ts
|
|
2958
|
-
import { z as
|
|
2959
|
-
var generateOptionsSchema =
|
|
2960
|
-
targets:
|
|
2961
|
-
features:
|
|
2962
|
-
delete:
|
|
2963
|
-
global:
|
|
2964
|
-
simulateCommands:
|
|
2965
|
-
simulateSubagents:
|
|
2966
|
-
simulateSkills:
|
|
4282
|
+
import { z as z9 } from "zod/mini";
|
|
4283
|
+
var generateOptionsSchema = z9.object({
|
|
4284
|
+
targets: z9.optional(z9.array(z9.string())),
|
|
4285
|
+
features: z9.optional(z9.array(z9.string())),
|
|
4286
|
+
delete: z9.optional(z9.boolean()),
|
|
4287
|
+
global: z9.optional(z9.boolean()),
|
|
4288
|
+
simulateCommands: z9.optional(z9.boolean()),
|
|
4289
|
+
simulateSubagents: z9.optional(z9.boolean()),
|
|
4290
|
+
simulateSkills: z9.optional(z9.boolean())
|
|
2967
4291
|
});
|
|
2968
4292
|
async function executeGenerate(options = {}) {
|
|
2969
4293
|
try {
|
|
@@ -3041,12 +4365,139 @@ var generateTools = {
|
|
|
3041
4365
|
}
|
|
3042
4366
|
};
|
|
3043
4367
|
|
|
4368
|
+
// src/mcp/hooks.ts
|
|
4369
|
+
import { join as join14 } from "path";
|
|
4370
|
+
import { z as z10 } from "zod/mini";
|
|
4371
|
+
var maxHooksSizeBytes = 1024 * 1024;
|
|
4372
|
+
async function getHooksFile() {
|
|
4373
|
+
try {
|
|
4374
|
+
const rulesyncHooks = await RulesyncHooks.fromFile({
|
|
4375
|
+
validate: true
|
|
4376
|
+
});
|
|
4377
|
+
const relativePathFromCwd = join14(
|
|
4378
|
+
rulesyncHooks.getRelativeDirPath(),
|
|
4379
|
+
rulesyncHooks.getRelativeFilePath()
|
|
4380
|
+
);
|
|
4381
|
+
return {
|
|
4382
|
+
relativePathFromCwd,
|
|
4383
|
+
content: rulesyncHooks.getFileContent()
|
|
4384
|
+
};
|
|
4385
|
+
} catch (error) {
|
|
4386
|
+
throw new Error(
|
|
4387
|
+
`Failed to read hooks file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4388
|
+
{
|
|
4389
|
+
cause: error
|
|
4390
|
+
}
|
|
4391
|
+
);
|
|
4392
|
+
}
|
|
4393
|
+
}
|
|
4394
|
+
async function putHooksFile({ content }) {
|
|
4395
|
+
if (content.length > maxHooksSizeBytes) {
|
|
4396
|
+
throw new Error(
|
|
4397
|
+
`Hooks file size ${content.length} bytes exceeds maximum ${maxHooksSizeBytes} bytes (1MB) for ${RULESYNC_HOOKS_RELATIVE_FILE_PATH}`
|
|
4398
|
+
);
|
|
4399
|
+
}
|
|
4400
|
+
try {
|
|
4401
|
+
JSON.parse(content);
|
|
4402
|
+
} catch (error) {
|
|
4403
|
+
throw new Error(
|
|
4404
|
+
`Invalid JSON format in hooks file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4405
|
+
{
|
|
4406
|
+
cause: error
|
|
4407
|
+
}
|
|
4408
|
+
);
|
|
4409
|
+
}
|
|
4410
|
+
try {
|
|
4411
|
+
const baseDir = process.cwd();
|
|
4412
|
+
const paths = RulesyncHooks.getSettablePaths();
|
|
4413
|
+
const relativeDirPath = paths.relativeDirPath;
|
|
4414
|
+
const relativeFilePath = paths.relativeFilePath;
|
|
4415
|
+
const fullPath = join14(baseDir, relativeDirPath, relativeFilePath);
|
|
4416
|
+
const rulesyncHooks = new RulesyncHooks({
|
|
4417
|
+
baseDir,
|
|
4418
|
+
relativeDirPath,
|
|
4419
|
+
relativeFilePath,
|
|
4420
|
+
fileContent: content,
|
|
4421
|
+
validate: true
|
|
4422
|
+
});
|
|
4423
|
+
await ensureDir(join14(baseDir, relativeDirPath));
|
|
4424
|
+
await writeFileContent(fullPath, content);
|
|
4425
|
+
const relativePathFromCwd = join14(relativeDirPath, relativeFilePath);
|
|
4426
|
+
return {
|
|
4427
|
+
relativePathFromCwd,
|
|
4428
|
+
content: rulesyncHooks.getFileContent()
|
|
4429
|
+
};
|
|
4430
|
+
} catch (error) {
|
|
4431
|
+
throw new Error(
|
|
4432
|
+
`Failed to write hooks file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4433
|
+
{
|
|
4434
|
+
cause: error
|
|
4435
|
+
}
|
|
4436
|
+
);
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4439
|
+
async function deleteHooksFile() {
|
|
4440
|
+
try {
|
|
4441
|
+
const baseDir = process.cwd();
|
|
4442
|
+
const paths = RulesyncHooks.getSettablePaths();
|
|
4443
|
+
const filePath = join14(baseDir, paths.relativeDirPath, paths.relativeFilePath);
|
|
4444
|
+
await removeFile(filePath);
|
|
4445
|
+
const relativePathFromCwd = join14(paths.relativeDirPath, paths.relativeFilePath);
|
|
4446
|
+
return {
|
|
4447
|
+
relativePathFromCwd
|
|
4448
|
+
};
|
|
4449
|
+
} catch (error) {
|
|
4450
|
+
throw new Error(
|
|
4451
|
+
`Failed to delete hooks file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4452
|
+
{
|
|
4453
|
+
cause: error
|
|
4454
|
+
}
|
|
4455
|
+
);
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
var hooksToolSchemas = {
|
|
4459
|
+
getHooksFile: z10.object({}),
|
|
4460
|
+
putHooksFile: z10.object({
|
|
4461
|
+
content: z10.string()
|
|
4462
|
+
}),
|
|
4463
|
+
deleteHooksFile: z10.object({})
|
|
4464
|
+
};
|
|
4465
|
+
var hooksTools = {
|
|
4466
|
+
getHooksFile: {
|
|
4467
|
+
name: "getHooksFile",
|
|
4468
|
+
description: `Get the hooks configuration file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}).`,
|
|
4469
|
+
parameters: hooksToolSchemas.getHooksFile,
|
|
4470
|
+
execute: async () => {
|
|
4471
|
+
const result = await getHooksFile();
|
|
4472
|
+
return JSON.stringify(result, null, 2);
|
|
4473
|
+
}
|
|
4474
|
+
},
|
|
4475
|
+
putHooksFile: {
|
|
4476
|
+
name: "putHooksFile",
|
|
4477
|
+
description: "Create or update the hooks configuration file (upsert operation). content parameter is required and must be valid JSON.",
|
|
4478
|
+
parameters: hooksToolSchemas.putHooksFile,
|
|
4479
|
+
execute: async (args) => {
|
|
4480
|
+
const result = await putHooksFile({ content: args.content });
|
|
4481
|
+
return JSON.stringify(result, null, 2);
|
|
4482
|
+
}
|
|
4483
|
+
},
|
|
4484
|
+
deleteHooksFile: {
|
|
4485
|
+
name: "deleteHooksFile",
|
|
4486
|
+
description: "Delete the hooks configuration file.",
|
|
4487
|
+
parameters: hooksToolSchemas.deleteHooksFile,
|
|
4488
|
+
execute: async () => {
|
|
4489
|
+
const result = await deleteHooksFile();
|
|
4490
|
+
return JSON.stringify(result, null, 2);
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
};
|
|
4494
|
+
|
|
3044
4495
|
// src/mcp/ignore.ts
|
|
3045
|
-
import { join as
|
|
3046
|
-
import { z as
|
|
4496
|
+
import { join as join15 } from "path";
|
|
4497
|
+
import { z as z11 } from "zod/mini";
|
|
3047
4498
|
var maxIgnoreFileSizeBytes = 100 * 1024;
|
|
3048
4499
|
async function getIgnoreFile() {
|
|
3049
|
-
const ignoreFilePath =
|
|
4500
|
+
const ignoreFilePath = join15(process.cwd(), RULESYNC_AIIGNORE_RELATIVE_FILE_PATH);
|
|
3050
4501
|
try {
|
|
3051
4502
|
const content = await readFileContent(ignoreFilePath);
|
|
3052
4503
|
return {
|
|
@@ -3063,7 +4514,7 @@ async function getIgnoreFile() {
|
|
|
3063
4514
|
}
|
|
3064
4515
|
}
|
|
3065
4516
|
async function putIgnoreFile({ content }) {
|
|
3066
|
-
const ignoreFilePath =
|
|
4517
|
+
const ignoreFilePath = join15(process.cwd(), RULESYNC_AIIGNORE_RELATIVE_FILE_PATH);
|
|
3067
4518
|
const contentSizeBytes = Buffer.byteLength(content, "utf8");
|
|
3068
4519
|
if (contentSizeBytes > maxIgnoreFileSizeBytes) {
|
|
3069
4520
|
throw new Error(
|
|
@@ -3087,8 +4538,8 @@ async function putIgnoreFile({ content }) {
|
|
|
3087
4538
|
}
|
|
3088
4539
|
}
|
|
3089
4540
|
async function deleteIgnoreFile() {
|
|
3090
|
-
const aiignorePath =
|
|
3091
|
-
const legacyIgnorePath =
|
|
4541
|
+
const aiignorePath = join15(process.cwd(), RULESYNC_AIIGNORE_RELATIVE_FILE_PATH);
|
|
4542
|
+
const legacyIgnorePath = join15(process.cwd(), RULESYNC_IGNORE_RELATIVE_FILE_PATH);
|
|
3092
4543
|
try {
|
|
3093
4544
|
await Promise.all([removeFile(aiignorePath), removeFile(legacyIgnorePath)]);
|
|
3094
4545
|
return {
|
|
@@ -3106,11 +4557,11 @@ async function deleteIgnoreFile() {
|
|
|
3106
4557
|
}
|
|
3107
4558
|
}
|
|
3108
4559
|
var ignoreToolSchemas = {
|
|
3109
|
-
getIgnoreFile:
|
|
3110
|
-
putIgnoreFile:
|
|
3111
|
-
content:
|
|
4560
|
+
getIgnoreFile: z11.object({}),
|
|
4561
|
+
putIgnoreFile: z11.object({
|
|
4562
|
+
content: z11.string()
|
|
3112
4563
|
}),
|
|
3113
|
-
deleteIgnoreFile:
|
|
4564
|
+
deleteIgnoreFile: z11.object({})
|
|
3114
4565
|
};
|
|
3115
4566
|
var ignoreTools = {
|
|
3116
4567
|
getIgnoreFile: {
|
|
@@ -3143,11 +4594,11 @@ var ignoreTools = {
|
|
|
3143
4594
|
};
|
|
3144
4595
|
|
|
3145
4596
|
// src/mcp/import.ts
|
|
3146
|
-
import { z as
|
|
3147
|
-
var importOptionsSchema =
|
|
3148
|
-
target:
|
|
3149
|
-
features:
|
|
3150
|
-
global:
|
|
4597
|
+
import { z as z12 } from "zod/mini";
|
|
4598
|
+
var importOptionsSchema = z12.object({
|
|
4599
|
+
target: z12.string(),
|
|
4600
|
+
features: z12.optional(z12.array(z12.string())),
|
|
4601
|
+
global: z12.optional(z12.boolean())
|
|
3151
4602
|
});
|
|
3152
4603
|
async function executeImport(options) {
|
|
3153
4604
|
try {
|
|
@@ -3218,15 +4669,15 @@ var importTools = {
|
|
|
3218
4669
|
};
|
|
3219
4670
|
|
|
3220
4671
|
// src/mcp/mcp.ts
|
|
3221
|
-
import { join as
|
|
3222
|
-
import { z as
|
|
4672
|
+
import { join as join16 } from "path";
|
|
4673
|
+
import { z as z13 } from "zod/mini";
|
|
3223
4674
|
var maxMcpSizeBytes = 1024 * 1024;
|
|
3224
4675
|
async function getMcpFile() {
|
|
3225
4676
|
try {
|
|
3226
4677
|
const rulesyncMcp = await RulesyncMcp.fromFile({
|
|
3227
4678
|
validate: true
|
|
3228
4679
|
});
|
|
3229
|
-
const relativePathFromCwd =
|
|
4680
|
+
const relativePathFromCwd = join16(
|
|
3230
4681
|
rulesyncMcp.getRelativeDirPath(),
|
|
3231
4682
|
rulesyncMcp.getRelativeFilePath()
|
|
3232
4683
|
);
|
|
@@ -3264,7 +4715,7 @@ async function putMcpFile({ content }) {
|
|
|
3264
4715
|
const paths = RulesyncMcp.getSettablePaths();
|
|
3265
4716
|
const relativeDirPath = paths.recommended.relativeDirPath;
|
|
3266
4717
|
const relativeFilePath = paths.recommended.relativeFilePath;
|
|
3267
|
-
const fullPath =
|
|
4718
|
+
const fullPath = join16(baseDir, relativeDirPath, relativeFilePath);
|
|
3268
4719
|
const rulesyncMcp = new RulesyncMcp({
|
|
3269
4720
|
baseDir,
|
|
3270
4721
|
relativeDirPath,
|
|
@@ -3272,9 +4723,9 @@ async function putMcpFile({ content }) {
|
|
|
3272
4723
|
fileContent: content,
|
|
3273
4724
|
validate: true
|
|
3274
4725
|
});
|
|
3275
|
-
await ensureDir(
|
|
4726
|
+
await ensureDir(join16(baseDir, relativeDirPath));
|
|
3276
4727
|
await writeFileContent(fullPath, content);
|
|
3277
|
-
const relativePathFromCwd =
|
|
4728
|
+
const relativePathFromCwd = join16(relativeDirPath, relativeFilePath);
|
|
3278
4729
|
return {
|
|
3279
4730
|
relativePathFromCwd,
|
|
3280
4731
|
content: rulesyncMcp.getFileContent()
|
|
@@ -3292,15 +4743,15 @@ async function deleteMcpFile() {
|
|
|
3292
4743
|
try {
|
|
3293
4744
|
const baseDir = process.cwd();
|
|
3294
4745
|
const paths = RulesyncMcp.getSettablePaths();
|
|
3295
|
-
const recommendedPath =
|
|
4746
|
+
const recommendedPath = join16(
|
|
3296
4747
|
baseDir,
|
|
3297
4748
|
paths.recommended.relativeDirPath,
|
|
3298
4749
|
paths.recommended.relativeFilePath
|
|
3299
4750
|
);
|
|
3300
|
-
const legacyPath =
|
|
4751
|
+
const legacyPath = join16(baseDir, paths.legacy.relativeDirPath, paths.legacy.relativeFilePath);
|
|
3301
4752
|
await removeFile(recommendedPath);
|
|
3302
4753
|
await removeFile(legacyPath);
|
|
3303
|
-
const relativePathFromCwd =
|
|
4754
|
+
const relativePathFromCwd = join16(
|
|
3304
4755
|
paths.recommended.relativeDirPath,
|
|
3305
4756
|
paths.recommended.relativeFilePath
|
|
3306
4757
|
);
|
|
@@ -3317,11 +4768,11 @@ async function deleteMcpFile() {
|
|
|
3317
4768
|
}
|
|
3318
4769
|
}
|
|
3319
4770
|
var mcpToolSchemas = {
|
|
3320
|
-
getMcpFile:
|
|
3321
|
-
putMcpFile:
|
|
3322
|
-
content:
|
|
4771
|
+
getMcpFile: z13.object({}),
|
|
4772
|
+
putMcpFile: z13.object({
|
|
4773
|
+
content: z13.string()
|
|
3323
4774
|
}),
|
|
3324
|
-
deleteMcpFile:
|
|
4775
|
+
deleteMcpFile: z13.object({})
|
|
3325
4776
|
};
|
|
3326
4777
|
var mcpTools = {
|
|
3327
4778
|
getMcpFile: {
|
|
@@ -3353,14 +4804,141 @@ var mcpTools = {
|
|
|
3353
4804
|
}
|
|
3354
4805
|
};
|
|
3355
4806
|
|
|
4807
|
+
// src/mcp/permissions.ts
|
|
4808
|
+
import { join as join17 } from "path";
|
|
4809
|
+
import { z as z14 } from "zod/mini";
|
|
4810
|
+
var maxPermissionsSizeBytes = 1024 * 1024;
|
|
4811
|
+
async function getPermissionsFile() {
|
|
4812
|
+
try {
|
|
4813
|
+
const rulesyncPermissions = await RulesyncPermissions.fromFile({
|
|
4814
|
+
validate: true
|
|
4815
|
+
});
|
|
4816
|
+
const relativePathFromCwd = join17(
|
|
4817
|
+
rulesyncPermissions.getRelativeDirPath(),
|
|
4818
|
+
rulesyncPermissions.getRelativeFilePath()
|
|
4819
|
+
);
|
|
4820
|
+
return {
|
|
4821
|
+
relativePathFromCwd,
|
|
4822
|
+
content: rulesyncPermissions.getFileContent()
|
|
4823
|
+
};
|
|
4824
|
+
} catch (error) {
|
|
4825
|
+
throw new Error(
|
|
4826
|
+
`Failed to read permissions file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4827
|
+
{
|
|
4828
|
+
cause: error
|
|
4829
|
+
}
|
|
4830
|
+
);
|
|
4831
|
+
}
|
|
4832
|
+
}
|
|
4833
|
+
async function putPermissionsFile({ content }) {
|
|
4834
|
+
if (content.length > maxPermissionsSizeBytes) {
|
|
4835
|
+
throw new Error(
|
|
4836
|
+
`Permissions file size ${content.length} bytes exceeds maximum ${maxPermissionsSizeBytes} bytes (1MB) for ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}`
|
|
4837
|
+
);
|
|
4838
|
+
}
|
|
4839
|
+
try {
|
|
4840
|
+
JSON.parse(content);
|
|
4841
|
+
} catch (error) {
|
|
4842
|
+
throw new Error(
|
|
4843
|
+
`Invalid JSON format in permissions file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4844
|
+
{
|
|
4845
|
+
cause: error
|
|
4846
|
+
}
|
|
4847
|
+
);
|
|
4848
|
+
}
|
|
4849
|
+
try {
|
|
4850
|
+
const baseDir = process.cwd();
|
|
4851
|
+
const paths = RulesyncPermissions.getSettablePaths();
|
|
4852
|
+
const relativeDirPath = paths.relativeDirPath;
|
|
4853
|
+
const relativeFilePath = paths.relativeFilePath;
|
|
4854
|
+
const fullPath = join17(baseDir, relativeDirPath, relativeFilePath);
|
|
4855
|
+
const rulesyncPermissions = new RulesyncPermissions({
|
|
4856
|
+
baseDir,
|
|
4857
|
+
relativeDirPath,
|
|
4858
|
+
relativeFilePath,
|
|
4859
|
+
fileContent: content,
|
|
4860
|
+
validate: true
|
|
4861
|
+
});
|
|
4862
|
+
await ensureDir(join17(baseDir, relativeDirPath));
|
|
4863
|
+
await writeFileContent(fullPath, content);
|
|
4864
|
+
const relativePathFromCwd = join17(relativeDirPath, relativeFilePath);
|
|
4865
|
+
return {
|
|
4866
|
+
relativePathFromCwd,
|
|
4867
|
+
content: rulesyncPermissions.getFileContent()
|
|
4868
|
+
};
|
|
4869
|
+
} catch (error) {
|
|
4870
|
+
throw new Error(
|
|
4871
|
+
`Failed to write permissions file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4872
|
+
{
|
|
4873
|
+
cause: error
|
|
4874
|
+
}
|
|
4875
|
+
);
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4878
|
+
async function deletePermissionsFile() {
|
|
4879
|
+
try {
|
|
4880
|
+
const baseDir = process.cwd();
|
|
4881
|
+
const paths = RulesyncPermissions.getSettablePaths();
|
|
4882
|
+
const filePath = join17(baseDir, paths.relativeDirPath, paths.relativeFilePath);
|
|
4883
|
+
await removeFile(filePath);
|
|
4884
|
+
const relativePathFromCwd = join17(paths.relativeDirPath, paths.relativeFilePath);
|
|
4885
|
+
return {
|
|
4886
|
+
relativePathFromCwd
|
|
4887
|
+
};
|
|
4888
|
+
} catch (error) {
|
|
4889
|
+
throw new Error(
|
|
4890
|
+
`Failed to delete permissions file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4891
|
+
{
|
|
4892
|
+
cause: error
|
|
4893
|
+
}
|
|
4894
|
+
);
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
var permissionsToolSchemas = {
|
|
4898
|
+
getPermissionsFile: z14.object({}),
|
|
4899
|
+
putPermissionsFile: z14.object({
|
|
4900
|
+
content: z14.string()
|
|
4901
|
+
}),
|
|
4902
|
+
deletePermissionsFile: z14.object({})
|
|
4903
|
+
};
|
|
4904
|
+
var permissionsTools = {
|
|
4905
|
+
getPermissionsFile: {
|
|
4906
|
+
name: "getPermissionsFile",
|
|
4907
|
+
description: `Get the permissions configuration file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}).`,
|
|
4908
|
+
parameters: permissionsToolSchemas.getPermissionsFile,
|
|
4909
|
+
execute: async () => {
|
|
4910
|
+
const result = await getPermissionsFile();
|
|
4911
|
+
return JSON.stringify(result, null, 2);
|
|
4912
|
+
}
|
|
4913
|
+
},
|
|
4914
|
+
putPermissionsFile: {
|
|
4915
|
+
name: "putPermissionsFile",
|
|
4916
|
+
description: "Create or update the permissions configuration file (upsert operation). content parameter is required and must be valid JSON.",
|
|
4917
|
+
parameters: permissionsToolSchemas.putPermissionsFile,
|
|
4918
|
+
execute: async (args) => {
|
|
4919
|
+
const result = await putPermissionsFile({ content: args.content });
|
|
4920
|
+
return JSON.stringify(result, null, 2);
|
|
4921
|
+
}
|
|
4922
|
+
},
|
|
4923
|
+
deletePermissionsFile: {
|
|
4924
|
+
name: "deletePermissionsFile",
|
|
4925
|
+
description: "Delete the permissions configuration file.",
|
|
4926
|
+
parameters: permissionsToolSchemas.deletePermissionsFile,
|
|
4927
|
+
execute: async () => {
|
|
4928
|
+
const result = await deletePermissionsFile();
|
|
4929
|
+
return JSON.stringify(result, null, 2);
|
|
4930
|
+
}
|
|
4931
|
+
}
|
|
4932
|
+
};
|
|
4933
|
+
|
|
3356
4934
|
// src/mcp/rules.ts
|
|
3357
|
-
import { basename as
|
|
3358
|
-
import { z as
|
|
4935
|
+
import { basename as basename3, join as join18 } from "path";
|
|
4936
|
+
import { z as z15 } from "zod/mini";
|
|
3359
4937
|
var logger2 = new ConsoleLogger({ verbose: false, silent: true });
|
|
3360
4938
|
var maxRuleSizeBytes = 1024 * 1024;
|
|
3361
4939
|
var maxRulesCount = 1e3;
|
|
3362
4940
|
async function listRules() {
|
|
3363
|
-
const rulesDir =
|
|
4941
|
+
const rulesDir = join18(process.cwd(), RULESYNC_RULES_RELATIVE_DIR_PATH);
|
|
3364
4942
|
try {
|
|
3365
4943
|
const files = await listDirectoryFiles(rulesDir);
|
|
3366
4944
|
const mdFiles = files.filter((file) => file.endsWith(".md"));
|
|
@@ -3373,7 +4951,7 @@ async function listRules() {
|
|
|
3373
4951
|
});
|
|
3374
4952
|
const frontmatter = rule.getFrontmatter();
|
|
3375
4953
|
return {
|
|
3376
|
-
relativePathFromCwd:
|
|
4954
|
+
relativePathFromCwd: join18(RULESYNC_RULES_RELATIVE_DIR_PATH, file),
|
|
3377
4955
|
frontmatter
|
|
3378
4956
|
};
|
|
3379
4957
|
} catch (error) {
|
|
@@ -3395,14 +4973,14 @@ async function getRule({ relativePathFromCwd }) {
|
|
|
3395
4973
|
relativePath: relativePathFromCwd,
|
|
3396
4974
|
intendedRootDir: process.cwd()
|
|
3397
4975
|
});
|
|
3398
|
-
const filename =
|
|
4976
|
+
const filename = basename3(relativePathFromCwd);
|
|
3399
4977
|
try {
|
|
3400
4978
|
const rule = await RulesyncRule.fromFile({
|
|
3401
4979
|
relativeFilePath: filename,
|
|
3402
4980
|
validate: true
|
|
3403
4981
|
});
|
|
3404
4982
|
return {
|
|
3405
|
-
relativePathFromCwd:
|
|
4983
|
+
relativePathFromCwd: join18(RULESYNC_RULES_RELATIVE_DIR_PATH, filename),
|
|
3406
4984
|
frontmatter: rule.getFrontmatter(),
|
|
3407
4985
|
body: rule.getBody()
|
|
3408
4986
|
};
|
|
@@ -3421,7 +4999,7 @@ async function putRule({
|
|
|
3421
4999
|
relativePath: relativePathFromCwd,
|
|
3422
5000
|
intendedRootDir: process.cwd()
|
|
3423
5001
|
});
|
|
3424
|
-
const filename =
|
|
5002
|
+
const filename = basename3(relativePathFromCwd);
|
|
3425
5003
|
const estimatedSize = JSON.stringify(frontmatter).length + body.length;
|
|
3426
5004
|
if (estimatedSize > maxRuleSizeBytes) {
|
|
3427
5005
|
throw new Error(
|
|
@@ -3431,7 +5009,7 @@ async function putRule({
|
|
|
3431
5009
|
try {
|
|
3432
5010
|
const existingRules = await listRules();
|
|
3433
5011
|
const isUpdate = existingRules.some(
|
|
3434
|
-
(rule2) => rule2.relativePathFromCwd ===
|
|
5012
|
+
(rule2) => rule2.relativePathFromCwd === join18(RULESYNC_RULES_RELATIVE_DIR_PATH, filename)
|
|
3435
5013
|
);
|
|
3436
5014
|
if (!isUpdate && existingRules.length >= maxRulesCount) {
|
|
3437
5015
|
throw new Error(
|
|
@@ -3446,11 +5024,11 @@ async function putRule({
|
|
|
3446
5024
|
body,
|
|
3447
5025
|
validate: true
|
|
3448
5026
|
});
|
|
3449
|
-
const rulesDir =
|
|
5027
|
+
const rulesDir = join18(process.cwd(), RULESYNC_RULES_RELATIVE_DIR_PATH);
|
|
3450
5028
|
await ensureDir(rulesDir);
|
|
3451
5029
|
await writeFileContent(rule.getFilePath(), rule.getFileContent());
|
|
3452
5030
|
return {
|
|
3453
|
-
relativePathFromCwd:
|
|
5031
|
+
relativePathFromCwd: join18(RULESYNC_RULES_RELATIVE_DIR_PATH, filename),
|
|
3454
5032
|
frontmatter: rule.getFrontmatter(),
|
|
3455
5033
|
body: rule.getBody()
|
|
3456
5034
|
};
|
|
@@ -3465,12 +5043,12 @@ async function deleteRule({ relativePathFromCwd }) {
|
|
|
3465
5043
|
relativePath: relativePathFromCwd,
|
|
3466
5044
|
intendedRootDir: process.cwd()
|
|
3467
5045
|
});
|
|
3468
|
-
const filename =
|
|
3469
|
-
const fullPath =
|
|
5046
|
+
const filename = basename3(relativePathFromCwd);
|
|
5047
|
+
const fullPath = join18(process.cwd(), RULESYNC_RULES_RELATIVE_DIR_PATH, filename);
|
|
3470
5048
|
try {
|
|
3471
5049
|
await removeFile(fullPath);
|
|
3472
5050
|
return {
|
|
3473
|
-
relativePathFromCwd:
|
|
5051
|
+
relativePathFromCwd: join18(RULESYNC_RULES_RELATIVE_DIR_PATH, filename)
|
|
3474
5052
|
};
|
|
3475
5053
|
} catch (error) {
|
|
3476
5054
|
throw new Error(`Failed to delete rule file ${relativePathFromCwd}: ${formatError(error)}`, {
|
|
@@ -3479,23 +5057,23 @@ async function deleteRule({ relativePathFromCwd }) {
|
|
|
3479
5057
|
}
|
|
3480
5058
|
}
|
|
3481
5059
|
var ruleToolSchemas = {
|
|
3482
|
-
listRules:
|
|
3483
|
-
getRule:
|
|
3484
|
-
relativePathFromCwd:
|
|
5060
|
+
listRules: z15.object({}),
|
|
5061
|
+
getRule: z15.object({
|
|
5062
|
+
relativePathFromCwd: z15.string()
|
|
3485
5063
|
}),
|
|
3486
|
-
putRule:
|
|
3487
|
-
relativePathFromCwd:
|
|
5064
|
+
putRule: z15.object({
|
|
5065
|
+
relativePathFromCwd: z15.string(),
|
|
3488
5066
|
frontmatter: RulesyncRuleFrontmatterSchema,
|
|
3489
|
-
body:
|
|
5067
|
+
body: z15.string()
|
|
3490
5068
|
}),
|
|
3491
|
-
deleteRule:
|
|
3492
|
-
relativePathFromCwd:
|
|
5069
|
+
deleteRule: z15.object({
|
|
5070
|
+
relativePathFromCwd: z15.string()
|
|
3493
5071
|
})
|
|
3494
5072
|
};
|
|
3495
5073
|
var ruleTools = {
|
|
3496
5074
|
listRules: {
|
|
3497
5075
|
name: "listRules",
|
|
3498
|
-
description: `List all rules from ${
|
|
5076
|
+
description: `List all rules from ${join18(RULESYNC_RULES_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
|
|
3499
5077
|
parameters: ruleToolSchemas.listRules,
|
|
3500
5078
|
execute: async () => {
|
|
3501
5079
|
const rules = await listRules();
|
|
@@ -3537,8 +5115,8 @@ var ruleTools = {
|
|
|
3537
5115
|
};
|
|
3538
5116
|
|
|
3539
5117
|
// src/mcp/skills.ts
|
|
3540
|
-
import { basename as
|
|
3541
|
-
import { z as
|
|
5118
|
+
import { basename as basename4, dirname, join as join19 } from "path";
|
|
5119
|
+
import { z as z16 } from "zod/mini";
|
|
3542
5120
|
var logger3 = new ConsoleLogger({ verbose: false, silent: true });
|
|
3543
5121
|
var maxSkillSizeBytes = 1024 * 1024;
|
|
3544
5122
|
var maxSkillsCount = 1e3;
|
|
@@ -3555,19 +5133,19 @@ function mcpSkillFileToAiDirFile(file) {
|
|
|
3555
5133
|
};
|
|
3556
5134
|
}
|
|
3557
5135
|
function extractDirName(relativeDirPathFromCwd) {
|
|
3558
|
-
const dirName =
|
|
5136
|
+
const dirName = basename4(relativeDirPathFromCwd);
|
|
3559
5137
|
if (!dirName) {
|
|
3560
5138
|
throw new Error(`Invalid path: ${relativeDirPathFromCwd}`);
|
|
3561
5139
|
}
|
|
3562
5140
|
return dirName;
|
|
3563
5141
|
}
|
|
3564
5142
|
async function listSkills() {
|
|
3565
|
-
const skillsDir =
|
|
5143
|
+
const skillsDir = join19(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH);
|
|
3566
5144
|
try {
|
|
3567
|
-
const skillDirPaths = await findFilesByGlobs(
|
|
5145
|
+
const skillDirPaths = await findFilesByGlobs(join19(skillsDir, "*"), { type: "dir" });
|
|
3568
5146
|
const skills = await Promise.all(
|
|
3569
5147
|
skillDirPaths.map(async (dirPath) => {
|
|
3570
|
-
const dirName =
|
|
5148
|
+
const dirName = basename4(dirPath);
|
|
3571
5149
|
if (!dirName) return null;
|
|
3572
5150
|
try {
|
|
3573
5151
|
const skill = await RulesyncSkill.fromDir({
|
|
@@ -3575,7 +5153,7 @@ async function listSkills() {
|
|
|
3575
5153
|
});
|
|
3576
5154
|
const frontmatter = skill.getFrontmatter();
|
|
3577
5155
|
return {
|
|
3578
|
-
relativeDirPathFromCwd:
|
|
5156
|
+
relativeDirPathFromCwd: join19(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
|
|
3579
5157
|
frontmatter
|
|
3580
5158
|
};
|
|
3581
5159
|
} catch (error) {
|
|
@@ -3603,7 +5181,7 @@ async function getSkill({ relativeDirPathFromCwd }) {
|
|
|
3603
5181
|
dirName
|
|
3604
5182
|
});
|
|
3605
5183
|
return {
|
|
3606
|
-
relativeDirPathFromCwd:
|
|
5184
|
+
relativeDirPathFromCwd: join19(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
|
|
3607
5185
|
frontmatter: skill.getFrontmatter(),
|
|
3608
5186
|
body: skill.getBody(),
|
|
3609
5187
|
otherFiles: skill.getOtherFiles().map(aiDirFileToMcpSkillFile)
|
|
@@ -3637,7 +5215,7 @@ async function putSkill({
|
|
|
3637
5215
|
try {
|
|
3638
5216
|
const existingSkills = await listSkills();
|
|
3639
5217
|
const isUpdate = existingSkills.some(
|
|
3640
|
-
(skill2) => skill2.relativeDirPathFromCwd ===
|
|
5218
|
+
(skill2) => skill2.relativeDirPathFromCwd === join19(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName)
|
|
3641
5219
|
);
|
|
3642
5220
|
if (!isUpdate && existingSkills.length >= maxSkillsCount) {
|
|
3643
5221
|
throw new Error(
|
|
@@ -3654,9 +5232,9 @@ async function putSkill({
|
|
|
3654
5232
|
otherFiles: aiDirFiles,
|
|
3655
5233
|
validate: true
|
|
3656
5234
|
});
|
|
3657
|
-
const skillDirPath =
|
|
5235
|
+
const skillDirPath = join19(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName);
|
|
3658
5236
|
await ensureDir(skillDirPath);
|
|
3659
|
-
const skillFilePath =
|
|
5237
|
+
const skillFilePath = join19(skillDirPath, SKILL_FILE_NAME);
|
|
3660
5238
|
const skillFileContent = stringifyFrontmatter(body, frontmatter);
|
|
3661
5239
|
await writeFileContent(skillFilePath, skillFileContent);
|
|
3662
5240
|
for (const file of otherFiles) {
|
|
@@ -3664,15 +5242,15 @@ async function putSkill({
|
|
|
3664
5242
|
relativePath: file.name,
|
|
3665
5243
|
intendedRootDir: skillDirPath
|
|
3666
5244
|
});
|
|
3667
|
-
const filePath =
|
|
3668
|
-
const fileDir =
|
|
5245
|
+
const filePath = join19(skillDirPath, file.name);
|
|
5246
|
+
const fileDir = join19(skillDirPath, dirname(file.name));
|
|
3669
5247
|
if (fileDir !== skillDirPath) {
|
|
3670
5248
|
await ensureDir(fileDir);
|
|
3671
5249
|
}
|
|
3672
5250
|
await writeFileContent(filePath, file.body);
|
|
3673
5251
|
}
|
|
3674
5252
|
return {
|
|
3675
|
-
relativeDirPathFromCwd:
|
|
5253
|
+
relativeDirPathFromCwd: join19(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
|
|
3676
5254
|
frontmatter: skill.getFrontmatter(),
|
|
3677
5255
|
body: skill.getBody(),
|
|
3678
5256
|
otherFiles: skill.getOtherFiles().map(aiDirFileToMcpSkillFile)
|
|
@@ -3694,13 +5272,13 @@ async function deleteSkill({
|
|
|
3694
5272
|
intendedRootDir: process.cwd()
|
|
3695
5273
|
});
|
|
3696
5274
|
const dirName = extractDirName(relativeDirPathFromCwd);
|
|
3697
|
-
const skillDirPath =
|
|
5275
|
+
const skillDirPath = join19(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName);
|
|
3698
5276
|
try {
|
|
3699
5277
|
if (await directoryExists(skillDirPath)) {
|
|
3700
5278
|
await removeDirectory(skillDirPath);
|
|
3701
5279
|
}
|
|
3702
5280
|
return {
|
|
3703
|
-
relativeDirPathFromCwd:
|
|
5281
|
+
relativeDirPathFromCwd: join19(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName)
|
|
3704
5282
|
};
|
|
3705
5283
|
} catch (error) {
|
|
3706
5284
|
throw new Error(
|
|
@@ -3711,29 +5289,29 @@ async function deleteSkill({
|
|
|
3711
5289
|
);
|
|
3712
5290
|
}
|
|
3713
5291
|
}
|
|
3714
|
-
var McpSkillFileSchema =
|
|
3715
|
-
name:
|
|
3716
|
-
body:
|
|
5292
|
+
var McpSkillFileSchema = z16.object({
|
|
5293
|
+
name: z16.string(),
|
|
5294
|
+
body: z16.string()
|
|
3717
5295
|
});
|
|
3718
5296
|
var skillToolSchemas = {
|
|
3719
|
-
listSkills:
|
|
3720
|
-
getSkill:
|
|
3721
|
-
relativeDirPathFromCwd:
|
|
5297
|
+
listSkills: z16.object({}),
|
|
5298
|
+
getSkill: z16.object({
|
|
5299
|
+
relativeDirPathFromCwd: z16.string()
|
|
3722
5300
|
}),
|
|
3723
|
-
putSkill:
|
|
3724
|
-
relativeDirPathFromCwd:
|
|
5301
|
+
putSkill: z16.object({
|
|
5302
|
+
relativeDirPathFromCwd: z16.string(),
|
|
3725
5303
|
frontmatter: RulesyncSkillFrontmatterSchema,
|
|
3726
|
-
body:
|
|
3727
|
-
otherFiles:
|
|
5304
|
+
body: z16.string(),
|
|
5305
|
+
otherFiles: z16.optional(z16.array(McpSkillFileSchema))
|
|
3728
5306
|
}),
|
|
3729
|
-
deleteSkill:
|
|
3730
|
-
relativeDirPathFromCwd:
|
|
5307
|
+
deleteSkill: z16.object({
|
|
5308
|
+
relativeDirPathFromCwd: z16.string()
|
|
3731
5309
|
})
|
|
3732
5310
|
};
|
|
3733
5311
|
var skillTools = {
|
|
3734
5312
|
listSkills: {
|
|
3735
5313
|
name: "listSkills",
|
|
3736
|
-
description: `List all skills from ${
|
|
5314
|
+
description: `List all skills from ${join19(RULESYNC_SKILLS_RELATIVE_DIR_PATH, "*", SKILL_FILE_NAME)} with their frontmatter.`,
|
|
3737
5315
|
parameters: skillToolSchemas.listSkills,
|
|
3738
5316
|
execute: async () => {
|
|
3739
5317
|
const skills = await listSkills();
|
|
@@ -3776,13 +5354,13 @@ var skillTools = {
|
|
|
3776
5354
|
};
|
|
3777
5355
|
|
|
3778
5356
|
// src/mcp/subagents.ts
|
|
3779
|
-
import { basename as
|
|
3780
|
-
import { z as
|
|
5357
|
+
import { basename as basename5, join as join20 } from "path";
|
|
5358
|
+
import { z as z17 } from "zod/mini";
|
|
3781
5359
|
var logger4 = new ConsoleLogger({ verbose: false, silent: true });
|
|
3782
5360
|
var maxSubagentSizeBytes = 1024 * 1024;
|
|
3783
5361
|
var maxSubagentsCount = 1e3;
|
|
3784
5362
|
async function listSubagents() {
|
|
3785
|
-
const subagentsDir =
|
|
5363
|
+
const subagentsDir = join20(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
|
|
3786
5364
|
try {
|
|
3787
5365
|
const files = await listDirectoryFiles(subagentsDir);
|
|
3788
5366
|
const mdFiles = files.filter((file) => file.endsWith(".md"));
|
|
@@ -3795,7 +5373,7 @@ async function listSubagents() {
|
|
|
3795
5373
|
});
|
|
3796
5374
|
const frontmatter = subagent.getFrontmatter();
|
|
3797
5375
|
return {
|
|
3798
|
-
relativePathFromCwd:
|
|
5376
|
+
relativePathFromCwd: join20(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, file),
|
|
3799
5377
|
frontmatter
|
|
3800
5378
|
};
|
|
3801
5379
|
} catch (error) {
|
|
@@ -3819,14 +5397,14 @@ async function getSubagent({ relativePathFromCwd }) {
|
|
|
3819
5397
|
relativePath: relativePathFromCwd,
|
|
3820
5398
|
intendedRootDir: process.cwd()
|
|
3821
5399
|
});
|
|
3822
|
-
const filename =
|
|
5400
|
+
const filename = basename5(relativePathFromCwd);
|
|
3823
5401
|
try {
|
|
3824
5402
|
const subagent = await RulesyncSubagent.fromFile({
|
|
3825
5403
|
relativeFilePath: filename,
|
|
3826
5404
|
validate: true
|
|
3827
5405
|
});
|
|
3828
5406
|
return {
|
|
3829
|
-
relativePathFromCwd:
|
|
5407
|
+
relativePathFromCwd: join20(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
|
|
3830
5408
|
frontmatter: subagent.getFrontmatter(),
|
|
3831
5409
|
body: subagent.getBody()
|
|
3832
5410
|
};
|
|
@@ -3845,7 +5423,7 @@ async function putSubagent({
|
|
|
3845
5423
|
relativePath: relativePathFromCwd,
|
|
3846
5424
|
intendedRootDir: process.cwd()
|
|
3847
5425
|
});
|
|
3848
|
-
const filename =
|
|
5426
|
+
const filename = basename5(relativePathFromCwd);
|
|
3849
5427
|
const estimatedSize = JSON.stringify(frontmatter).length + body.length;
|
|
3850
5428
|
if (estimatedSize > maxSubagentSizeBytes) {
|
|
3851
5429
|
throw new Error(
|
|
@@ -3855,7 +5433,7 @@ async function putSubagent({
|
|
|
3855
5433
|
try {
|
|
3856
5434
|
const existingSubagents = await listSubagents();
|
|
3857
5435
|
const isUpdate = existingSubagents.some(
|
|
3858
|
-
(subagent2) => subagent2.relativePathFromCwd ===
|
|
5436
|
+
(subagent2) => subagent2.relativePathFromCwd === join20(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
|
|
3859
5437
|
);
|
|
3860
5438
|
if (!isUpdate && existingSubagents.length >= maxSubagentsCount) {
|
|
3861
5439
|
throw new Error(
|
|
@@ -3870,11 +5448,11 @@ async function putSubagent({
|
|
|
3870
5448
|
body,
|
|
3871
5449
|
validate: true
|
|
3872
5450
|
});
|
|
3873
|
-
const subagentsDir =
|
|
5451
|
+
const subagentsDir = join20(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
|
|
3874
5452
|
await ensureDir(subagentsDir);
|
|
3875
5453
|
await writeFileContent(subagent.getFilePath(), subagent.getFileContent());
|
|
3876
5454
|
return {
|
|
3877
|
-
relativePathFromCwd:
|
|
5455
|
+
relativePathFromCwd: join20(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
|
|
3878
5456
|
frontmatter: subagent.getFrontmatter(),
|
|
3879
5457
|
body: subagent.getBody()
|
|
3880
5458
|
};
|
|
@@ -3889,12 +5467,12 @@ async function deleteSubagent({ relativePathFromCwd }) {
|
|
|
3889
5467
|
relativePath: relativePathFromCwd,
|
|
3890
5468
|
intendedRootDir: process.cwd()
|
|
3891
5469
|
});
|
|
3892
|
-
const filename =
|
|
3893
|
-
const fullPath =
|
|
5470
|
+
const filename = basename5(relativePathFromCwd);
|
|
5471
|
+
const fullPath = join20(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename);
|
|
3894
5472
|
try {
|
|
3895
5473
|
await removeFile(fullPath);
|
|
3896
5474
|
return {
|
|
3897
|
-
relativePathFromCwd:
|
|
5475
|
+
relativePathFromCwd: join20(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
|
|
3898
5476
|
};
|
|
3899
5477
|
} catch (error) {
|
|
3900
5478
|
throw new Error(
|
|
@@ -3906,23 +5484,23 @@ async function deleteSubagent({ relativePathFromCwd }) {
|
|
|
3906
5484
|
}
|
|
3907
5485
|
}
|
|
3908
5486
|
var subagentToolSchemas = {
|
|
3909
|
-
listSubagents:
|
|
3910
|
-
getSubagent:
|
|
3911
|
-
relativePathFromCwd:
|
|
5487
|
+
listSubagents: z17.object({}),
|
|
5488
|
+
getSubagent: z17.object({
|
|
5489
|
+
relativePathFromCwd: z17.string()
|
|
3912
5490
|
}),
|
|
3913
|
-
putSubagent:
|
|
3914
|
-
relativePathFromCwd:
|
|
5491
|
+
putSubagent: z17.object({
|
|
5492
|
+
relativePathFromCwd: z17.string(),
|
|
3915
5493
|
frontmatter: RulesyncSubagentFrontmatterSchema,
|
|
3916
|
-
body:
|
|
5494
|
+
body: z17.string()
|
|
3917
5495
|
}),
|
|
3918
|
-
deleteSubagent:
|
|
3919
|
-
relativePathFromCwd:
|
|
5496
|
+
deleteSubagent: z17.object({
|
|
5497
|
+
relativePathFromCwd: z17.string()
|
|
3920
5498
|
})
|
|
3921
5499
|
};
|
|
3922
5500
|
var subagentTools = {
|
|
3923
5501
|
listSubagents: {
|
|
3924
5502
|
name: "listSubagents",
|
|
3925
|
-
description: `List all subagents from ${
|
|
5503
|
+
description: `List all subagents from ${join20(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
|
|
3926
5504
|
parameters: subagentToolSchemas.listSubagents,
|
|
3927
5505
|
execute: async () => {
|
|
3928
5506
|
const subagents = await listSubagents();
|
|
@@ -3964,31 +5542,33 @@ var subagentTools = {
|
|
|
3964
5542
|
};
|
|
3965
5543
|
|
|
3966
5544
|
// src/mcp/tools.ts
|
|
3967
|
-
var rulesyncFeatureSchema =
|
|
5545
|
+
var rulesyncFeatureSchema = z18.enum([
|
|
3968
5546
|
"rule",
|
|
3969
5547
|
"command",
|
|
3970
5548
|
"subagent",
|
|
3971
5549
|
"skill",
|
|
3972
5550
|
"ignore",
|
|
3973
5551
|
"mcp",
|
|
5552
|
+
"permissions",
|
|
5553
|
+
"hooks",
|
|
3974
5554
|
"generate",
|
|
3975
5555
|
"import"
|
|
3976
5556
|
]);
|
|
3977
|
-
var rulesyncOperationSchema =
|
|
3978
|
-
var skillFileSchema =
|
|
3979
|
-
name:
|
|
3980
|
-
body:
|
|
5557
|
+
var rulesyncOperationSchema = z18.enum(["list", "get", "put", "delete", "run"]);
|
|
5558
|
+
var skillFileSchema = z18.object({
|
|
5559
|
+
name: z18.string(),
|
|
5560
|
+
body: z18.string()
|
|
3981
5561
|
});
|
|
3982
|
-
var rulesyncToolSchema =
|
|
5562
|
+
var rulesyncToolSchema = z18.object({
|
|
3983
5563
|
feature: rulesyncFeatureSchema,
|
|
3984
5564
|
operation: rulesyncOperationSchema,
|
|
3985
|
-
targetPathFromCwd:
|
|
3986
|
-
frontmatter:
|
|
3987
|
-
body:
|
|
3988
|
-
otherFiles:
|
|
3989
|
-
content:
|
|
3990
|
-
generateOptions:
|
|
3991
|
-
importOptions:
|
|
5565
|
+
targetPathFromCwd: z18.optional(z18.string()),
|
|
5566
|
+
frontmatter: z18.optional(z18.unknown()),
|
|
5567
|
+
body: z18.optional(z18.string()),
|
|
5568
|
+
otherFiles: z18.optional(z18.array(skillFileSchema)),
|
|
5569
|
+
content: z18.optional(z18.string()),
|
|
5570
|
+
generateOptions: z18.optional(generateOptionsSchema),
|
|
5571
|
+
importOptions: z18.optional(importOptionsSchema)
|
|
3992
5572
|
});
|
|
3993
5573
|
var supportedOperationsByFeature = {
|
|
3994
5574
|
rule: ["list", "get", "put", "delete"],
|
|
@@ -3997,6 +5577,8 @@ var supportedOperationsByFeature = {
|
|
|
3997
5577
|
skill: ["list", "get", "put", "delete"],
|
|
3998
5578
|
ignore: ["get", "put", "delete"],
|
|
3999
5579
|
mcp: ["get", "put", "delete"],
|
|
5580
|
+
permissions: ["get", "put", "delete"],
|
|
5581
|
+
hooks: ["get", "put", "delete"],
|
|
4000
5582
|
generate: ["run"],
|
|
4001
5583
|
import: ["run"]
|
|
4002
5584
|
};
|
|
@@ -4046,7 +5628,7 @@ function ensureBody({ body, feature, operation }) {
|
|
|
4046
5628
|
}
|
|
4047
5629
|
var rulesyncTool = {
|
|
4048
5630
|
name: "rulesyncTool",
|
|
4049
|
-
description: "Manage Rulesync files through a single MCP tool. Features: rule/command/subagent/skill support list/get/put/delete; ignore/mcp support get/put/delete only; generate supports run only; import 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); generate/run uses generateOptions to configure generation; import/run uses importOptions to configure import.",
|
|
5631
|
+
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. 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.",
|
|
4050
5632
|
parameters: rulesyncToolSchema,
|
|
4051
5633
|
execute: async (args) => {
|
|
4052
5634
|
const parsed = rulesyncToolSchema.parse(args);
|
|
@@ -4163,6 +5745,30 @@ var rulesyncTool = {
|
|
|
4163
5745
|
}
|
|
4164
5746
|
return mcpTools.deleteMcpFile.execute();
|
|
4165
5747
|
}
|
|
5748
|
+
case "permissions": {
|
|
5749
|
+
if (parsed.operation === "get") {
|
|
5750
|
+
return permissionsTools.getPermissionsFile.execute();
|
|
5751
|
+
}
|
|
5752
|
+
if (parsed.operation === "put") {
|
|
5753
|
+
if (!parsed.content) {
|
|
5754
|
+
throw new Error("content is required for permissions put operation");
|
|
5755
|
+
}
|
|
5756
|
+
return permissionsTools.putPermissionsFile.execute({ content: parsed.content });
|
|
5757
|
+
}
|
|
5758
|
+
return permissionsTools.deletePermissionsFile.execute();
|
|
5759
|
+
}
|
|
5760
|
+
case "hooks": {
|
|
5761
|
+
if (parsed.operation === "get") {
|
|
5762
|
+
return hooksTools.getHooksFile.execute();
|
|
5763
|
+
}
|
|
5764
|
+
if (parsed.operation === "put") {
|
|
5765
|
+
if (!parsed.content) {
|
|
5766
|
+
throw new Error("content is required for hooks put operation");
|
|
5767
|
+
}
|
|
5768
|
+
return hooksTools.putHooksFile.execute({ content: parsed.content });
|
|
5769
|
+
}
|
|
5770
|
+
return hooksTools.deleteHooksFile.execute();
|
|
5771
|
+
}
|
|
4166
5772
|
case "generate": {
|
|
4167
5773
|
return generateTools.executeGenerate.execute(parsed.generateOptions ?? {});
|
|
4168
5774
|
}
|
|
@@ -4195,7 +5801,7 @@ async function mcpCommand(logger5, { version }) {
|
|
|
4195
5801
|
}
|
|
4196
5802
|
|
|
4197
5803
|
// src/cli/commands/resolve-gitignore-targets.ts
|
|
4198
|
-
import { join as
|
|
5804
|
+
import { join as join21 } from "path";
|
|
4199
5805
|
var resolveGitignoreTargets = async ({
|
|
4200
5806
|
cliTargets,
|
|
4201
5807
|
cwd = process.cwd()
|
|
@@ -4203,8 +5809,8 @@ var resolveGitignoreTargets = async ({
|
|
|
4203
5809
|
if (cliTargets !== void 0) {
|
|
4204
5810
|
return cliTargets;
|
|
4205
5811
|
}
|
|
4206
|
-
const baseConfigPath =
|
|
4207
|
-
const localConfigPath =
|
|
5812
|
+
const baseConfigPath = join21(cwd, RULESYNC_CONFIG_RELATIVE_FILE_PATH);
|
|
5813
|
+
const localConfigPath = join21(cwd, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
|
|
4208
5814
|
const [hasBase, hasLocal] = await Promise.all([
|
|
4209
5815
|
fileExists(baseConfigPath),
|
|
4210
5816
|
fileExists(localConfigPath)
|
|
@@ -4625,7 +6231,7 @@ function wrapCommand({
|
|
|
4625
6231
|
}
|
|
4626
6232
|
|
|
4627
6233
|
// src/cli/index.ts
|
|
4628
|
-
var getVersion = () => "8.
|
|
6234
|
+
var getVersion = () => "8.7.0";
|
|
4629
6235
|
function wrapCommand2(name, errorCode, handler) {
|
|
4630
6236
|
return wrapCommand({ name, errorCode, handler, getVersion });
|
|
4631
6237
|
}
|
|
@@ -4691,12 +6297,18 @@ var main = async () => {
|
|
|
4691
6297
|
await mcpCommand(logger5, { version });
|
|
4692
6298
|
})
|
|
4693
6299
|
);
|
|
4694
|
-
program.command("install").description("Install skills from declarative sources
|
|
6300
|
+
program.command("install").description("Install skills/primitives from declarative sources (rulesync.jsonc) or apm.yml").option(
|
|
6301
|
+
"--mode <mode>",
|
|
6302
|
+
`Install layout to produce (${INSTALL_MODES.join("|")}). Default: rulesync`
|
|
6303
|
+
).option("--update", "Force re-resolve all source refs, ignoring lockfile").option(
|
|
4695
6304
|
"--frozen",
|
|
4696
6305
|
"Fail if lockfile is missing or out of sync (for CI); fetches missing skills using locked refs"
|
|
4697
6306
|
).option("--token <token>", "GitHub token for private repos").option("-c, --config <path>", "Path to configuration file").option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").action(
|
|
4698
6307
|
wrapCommand2("install", "INSTALL_FAILED", async (logger5, options) => {
|
|
6308
|
+
const rawMode = options.mode;
|
|
6309
|
+
const mode = parseInstallMode(rawMode);
|
|
4699
6310
|
await installCommand(logger5, {
|
|
6311
|
+
mode,
|
|
4700
6312
|
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
4701
6313
|
update: options.update,
|
|
4702
6314
|
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
@@ -4745,6 +6357,14 @@ var main = async () => {
|
|
|
4745
6357
|
);
|
|
4746
6358
|
program.parse();
|
|
4747
6359
|
};
|
|
6360
|
+
function parseInstallMode(raw) {
|
|
6361
|
+
if (raw === void 0) return void 0;
|
|
6362
|
+
const match = INSTALL_MODES.find((m) => m === raw);
|
|
6363
|
+
if (!match) {
|
|
6364
|
+
throw new Error(`Invalid --mode value "${raw}". Expected one of: ${INSTALL_MODES.join(", ")}.`);
|
|
6365
|
+
}
|
|
6366
|
+
return match;
|
|
6367
|
+
}
|
|
4748
6368
|
main().catch((error) => {
|
|
4749
6369
|
console.error(formatError(error));
|
|
4750
6370
|
process.exit(1);
|