rulesync 8.6.0 → 8.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/{chunk-ILMHM7BF.js → chunk-XIMWQREW.js} +1521 -987
- package/dist/cli/index.cjs +3563 -1398
- package/dist/cli/index.js +1851 -214
- package/dist/index.cjs +1537 -1006
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- 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-XIMWQREW.js";
|
|
78
81
|
|
|
79
82
|
// src/cli/index.ts
|
|
80
83
|
import { Command } from "commander";
|
|
@@ -1337,6 +1340,16 @@ var GITIGNORE_ENTRY_REGISTRY = [
|
|
|
1337
1340
|
entry: "**/.rovodev/.rulesync/"
|
|
1338
1341
|
},
|
|
1339
1342
|
{ target: "rovodev", feature: "skills", entry: "**/.agents/skills/" },
|
|
1343
|
+
// TAKT
|
|
1344
|
+
// Each rulesync feature maps one-to-one onto a TAKT facet directory.
|
|
1345
|
+
{ target: "takt", feature: "rules", entry: "**/.takt/facets/policies/" },
|
|
1346
|
+
{ target: "takt", feature: "skills", entry: "**/.takt/facets/knowledge/" },
|
|
1347
|
+
{ target: "takt", feature: "subagents", entry: "**/.takt/facets/personas/" },
|
|
1348
|
+
{ target: "takt", feature: "commands", entry: "**/.takt/facets/instructions/" },
|
|
1349
|
+
{ target: "takt", feature: "general", entry: "**/.takt/runs/" },
|
|
1350
|
+
{ target: "takt", feature: "general", entry: "**/.takt/tasks/" },
|
|
1351
|
+
{ target: "takt", feature: "general", entry: "**/.takt/.cache/" },
|
|
1352
|
+
{ target: "takt", feature: "general", entry: "**/.takt/config.yaml" },
|
|
1340
1353
|
// Windsurf
|
|
1341
1354
|
{ target: "windsurf", feature: "skills", entry: "**/.windsurf/skills/" },
|
|
1342
1355
|
{ target: "windsurf", feature: "skills", entry: "**/.codeium/windsurf/skills/" },
|
|
@@ -1344,9 +1357,16 @@ var GITIGNORE_ENTRY_REGISTRY = [
|
|
|
1344
1357
|
{ target: "warp", feature: "rules", entry: "**/.warp/" },
|
|
1345
1358
|
{ target: "warp", feature: "rules", entry: "**/WARP.md" }
|
|
1346
1359
|
];
|
|
1347
|
-
var ALL_GITIGNORE_ENTRIES =
|
|
1348
|
-
(
|
|
1349
|
-
|
|
1360
|
+
var ALL_GITIGNORE_ENTRIES = (() => {
|
|
1361
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1362
|
+
const result = [];
|
|
1363
|
+
for (const tag of GITIGNORE_ENTRY_REGISTRY) {
|
|
1364
|
+
if (seen.has(tag.entry)) continue;
|
|
1365
|
+
seen.add(tag.entry);
|
|
1366
|
+
result.push(tag.entry);
|
|
1367
|
+
}
|
|
1368
|
+
return result;
|
|
1369
|
+
})();
|
|
1350
1370
|
var isTargetSelected = (target, selectedTargets) => {
|
|
1351
1371
|
const targets = normalizeGitignoreEntryTargets(target);
|
|
1352
1372
|
if (targets.includes("common")) return true;
|
|
@@ -1948,13 +1968,1171 @@ async function initCommand(logger5) {
|
|
|
1948
1968
|
logger5.info("2. Run 'rulesync generate' to create configuration files");
|
|
1949
1969
|
}
|
|
1950
1970
|
|
|
1951
|
-
// src/lib/
|
|
1952
|
-
import {
|
|
1971
|
+
// src/lib/apm/apm-install.ts
|
|
1972
|
+
import { createHash } from "crypto";
|
|
1973
|
+
import { join as join6, posix as posix2 } from "path";
|
|
1953
1974
|
import { Semaphore as Semaphore2 } from "es-toolkit/promise";
|
|
1954
1975
|
|
|
1976
|
+
// src/lib/apm/apm-lock.ts
|
|
1977
|
+
import { join as join4 } from "path";
|
|
1978
|
+
import { dump, load } from "js-yaml";
|
|
1979
|
+
import { nonnegative, optional, refine, z as z4 } from "zod/mini";
|
|
1980
|
+
var APM_LOCKFILE_FILE_NAME = "rulesync-apm.lock.yaml";
|
|
1981
|
+
var APM_LOCKFILE_VERSION = "1";
|
|
1982
|
+
var RULESYNC_CONTENT_HASH_REGEX = /^sha256:[0-9a-f]{64}$/;
|
|
1983
|
+
var ApmLockDependencySchema = z4.looseObject({
|
|
1984
|
+
repo_url: z4.string(),
|
|
1985
|
+
resolved_commit: optional(
|
|
1986
|
+
z4.string().check(refine((v) => /^[0-9a-f]{40}$/.test(v), "resolved_commit must be a 40-char hex SHA"))
|
|
1987
|
+
),
|
|
1988
|
+
resolved_ref: optional(z4.string()),
|
|
1989
|
+
version: optional(z4.string()),
|
|
1990
|
+
depth: z4.int().check(nonnegative()),
|
|
1991
|
+
resolved_by: optional(z4.string()),
|
|
1992
|
+
package_type: z4.string(),
|
|
1993
|
+
// Intentionally loose: the upstream `apm` CLI may write content_hash values
|
|
1994
|
+
// that do not match the strict rulesync format. We accept any string on read
|
|
1995
|
+
// so that a lockfile produced by `apm` round-trips through rulesync without
|
|
1996
|
+
// throwing. Rulesync itself always writes values matching
|
|
1997
|
+
// `RULESYNC_CONTENT_HASH_REGEX`, and `--frozen` integrity checks only
|
|
1998
|
+
// enforce the comparison when the recorded hash matches that shape.
|
|
1999
|
+
content_hash: optional(z4.string()),
|
|
2000
|
+
is_dev: optional(z4.boolean()),
|
|
2001
|
+
deployed_files: z4.array(z4.string()),
|
|
2002
|
+
source: optional(z4.string()),
|
|
2003
|
+
local_path: optional(z4.string()),
|
|
2004
|
+
virtual_path: optional(z4.string()),
|
|
2005
|
+
is_virtual: optional(z4.boolean())
|
|
2006
|
+
});
|
|
2007
|
+
var ApmLockSchema = z4.looseObject({
|
|
2008
|
+
lockfile_version: z4.literal("1"),
|
|
2009
|
+
generated_at: z4.string(),
|
|
2010
|
+
apm_version: z4.string(),
|
|
2011
|
+
dependencies: z4.array(ApmLockDependencySchema),
|
|
2012
|
+
mcp_servers: optional(z4.array(z4.string()))
|
|
2013
|
+
});
|
|
2014
|
+
function getApmLockPath(baseDir) {
|
|
2015
|
+
return join4(baseDir, APM_LOCKFILE_FILE_NAME);
|
|
2016
|
+
}
|
|
2017
|
+
function createEmptyApmLock(params) {
|
|
2018
|
+
const base = params.existingLock ? { ...params.existingLock } : {};
|
|
2019
|
+
return {
|
|
2020
|
+
...base,
|
|
2021
|
+
lockfile_version: APM_LOCKFILE_VERSION,
|
|
2022
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2023
|
+
apm_version: params.apmVersion,
|
|
2024
|
+
dependencies: []
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
function parseApmLock(content) {
|
|
2028
|
+
if (!content.trim()) {
|
|
2029
|
+
return null;
|
|
2030
|
+
}
|
|
2031
|
+
let loaded;
|
|
2032
|
+
try {
|
|
2033
|
+
loaded = load(content);
|
|
2034
|
+
} catch {
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
2037
|
+
if (!loaded || typeof loaded !== "object") {
|
|
2038
|
+
return null;
|
|
2039
|
+
}
|
|
2040
|
+
const parsed = ApmLockSchema.safeParse(loaded);
|
|
2041
|
+
if (!parsed.success) {
|
|
2042
|
+
const issues = parsed.error.issues.map((issue) => ` - ${issue.path.join(".") || "<root>"}: ${issue.message}`).join("\n");
|
|
2043
|
+
throw new Error(`Invalid ${APM_LOCKFILE_FILE_NAME}:
|
|
2044
|
+
${issues}`);
|
|
2045
|
+
}
|
|
2046
|
+
return parsed.data;
|
|
2047
|
+
}
|
|
2048
|
+
async function readApmLock(baseDir) {
|
|
2049
|
+
const path2 = getApmLockPath(baseDir);
|
|
2050
|
+
if (!await fileExists(path2)) {
|
|
2051
|
+
return null;
|
|
2052
|
+
}
|
|
2053
|
+
const content = await readFileContent(path2);
|
|
2054
|
+
return parseApmLock(content);
|
|
2055
|
+
}
|
|
2056
|
+
async function writeApmLock(params) {
|
|
2057
|
+
const path2 = getApmLockPath(params.baseDir);
|
|
2058
|
+
const content = serializeApmLock(params.lock);
|
|
2059
|
+
await writeFileContent(path2, content);
|
|
2060
|
+
}
|
|
2061
|
+
function serializeApmLock(lock) {
|
|
2062
|
+
return dump(lock, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2063
|
+
}
|
|
2064
|
+
function findApmLockDependency(lock, repoUrl) {
|
|
2065
|
+
const target = repoUrl.toLowerCase();
|
|
2066
|
+
return lock.dependencies.find((d) => d.repo_url.toLowerCase() === target);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// src/lib/apm/apm-manifest.ts
|
|
2070
|
+
import { join as join5 } from "path";
|
|
2071
|
+
import { dump as dump2, load as load2 } from "js-yaml";
|
|
2072
|
+
import { optional as optional2, z as z5 } from "zod/mini";
|
|
2073
|
+
var APM_MANIFEST_FILE_NAME = "apm.yml";
|
|
2074
|
+
var ApmObjectDependencySchema = z5.looseObject({
|
|
2075
|
+
git: optional2(z5.string()),
|
|
2076
|
+
source: optional2(z5.string()),
|
|
2077
|
+
path: optional2(z5.string()),
|
|
2078
|
+
ref: optional2(z5.string()),
|
|
2079
|
+
alias: optional2(z5.string())
|
|
2080
|
+
});
|
|
2081
|
+
var ApmDependencyInputSchema = z5.union([z5.string(), ApmObjectDependencySchema]);
|
|
2082
|
+
var ApmManifestSchema = z5.looseObject({
|
|
2083
|
+
name: optional2(z5.string()),
|
|
2084
|
+
version: optional2(z5.string()),
|
|
2085
|
+
dependencies: optional2(
|
|
2086
|
+
z5.looseObject({
|
|
2087
|
+
apm: optional2(z5.array(ApmDependencyInputSchema))
|
|
2088
|
+
})
|
|
2089
|
+
)
|
|
2090
|
+
});
|
|
2091
|
+
function getApmManifestPath(baseDir) {
|
|
2092
|
+
return join5(baseDir, APM_MANIFEST_FILE_NAME);
|
|
2093
|
+
}
|
|
2094
|
+
async function apmManifestExists(baseDir) {
|
|
2095
|
+
return fileExists(getApmManifestPath(baseDir));
|
|
2096
|
+
}
|
|
2097
|
+
function parseApmManifest(content) {
|
|
2098
|
+
const loaded = load2(content);
|
|
2099
|
+
if (loaded === void 0 || loaded === null) {
|
|
2100
|
+
return { dependencies: [] };
|
|
2101
|
+
}
|
|
2102
|
+
const parsed = ApmManifestSchema.safeParse(loaded);
|
|
2103
|
+
if (!parsed.success) {
|
|
2104
|
+
throw new Error(`Invalid apm.yml: ${parsed.error.message}`);
|
|
2105
|
+
}
|
|
2106
|
+
const raw = parsed.data;
|
|
2107
|
+
const rawDeps = raw.dependencies?.apm ?? [];
|
|
2108
|
+
const dependencies = rawDeps.map(
|
|
2109
|
+
(entry, index) => normalizeDependency(entry, index)
|
|
2110
|
+
);
|
|
2111
|
+
return {
|
|
2112
|
+
name: raw.name,
|
|
2113
|
+
version: raw.version,
|
|
2114
|
+
dependencies
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
async function readApmManifest(baseDir) {
|
|
2118
|
+
const path2 = getApmManifestPath(baseDir);
|
|
2119
|
+
const content = await readFileContent(path2);
|
|
2120
|
+
return parseApmManifest(content);
|
|
2121
|
+
}
|
|
2122
|
+
function normalizeDependency(entry, index) {
|
|
2123
|
+
if (typeof entry === "string") {
|
|
2124
|
+
return normalizeStringDependency(entry, index);
|
|
2125
|
+
}
|
|
2126
|
+
const gitUrl = entry.git ?? entry.source;
|
|
2127
|
+
if (!gitUrl) {
|
|
2128
|
+
throw new Error(
|
|
2129
|
+
`apm.yml dependency #${index + 1}: object form requires a "git" field. Received: ${JSON.stringify(entry)}.`
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
const parsedUrl = parseHttpsGitHubUrl(gitUrl);
|
|
2133
|
+
if (!parsedUrl) {
|
|
2134
|
+
throw new Error(
|
|
2135
|
+
`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.`
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
if (entry.path !== void 0) {
|
|
2139
|
+
validateSubPath(entry.path, index);
|
|
2140
|
+
}
|
|
2141
|
+
return {
|
|
2142
|
+
gitUrl: parsedUrl.gitUrl,
|
|
2143
|
+
owner: parsedUrl.owner,
|
|
2144
|
+
repo: parsedUrl.repo,
|
|
2145
|
+
ref: entry.ref,
|
|
2146
|
+
path: entry.path,
|
|
2147
|
+
alias: entry.alias
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
function validateSubPath(subPath, index) {
|
|
2151
|
+
if (subPath === "" || subPath.startsWith("/") || subPath.startsWith("\\")) {
|
|
2152
|
+
throw new Error(
|
|
2153
|
+
`apm.yml dependency #${index + 1}: "path" must be a non-empty relative path without a leading slash. Received: ${JSON.stringify(subPath)}.`
|
|
2154
|
+
);
|
|
2155
|
+
}
|
|
2156
|
+
const segments = subPath.split(/[/\\]/);
|
|
2157
|
+
if (segments.includes("..")) {
|
|
2158
|
+
throw new Error(
|
|
2159
|
+
`apm.yml dependency #${index + 1}: "path" must not contain ".." segments. Received: ${JSON.stringify(subPath)}.`
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
function normalizeStringDependency(entry, index) {
|
|
2164
|
+
const trimmed = entry.trim();
|
|
2165
|
+
if (!trimmed) {
|
|
2166
|
+
throw new Error(`apm.yml dependency #${index + 1}: entry must be a non-empty string.`);
|
|
2167
|
+
}
|
|
2168
|
+
rejectUnsupportedShorthand(trimmed, index);
|
|
2169
|
+
if (trimmed.startsWith("https://")) {
|
|
2170
|
+
const [urlPart, refPart2] = splitOnFirst(trimmed, "#");
|
|
2171
|
+
const parsed = parseHttpsGitHubUrl(urlPart);
|
|
2172
|
+
if (!parsed) {
|
|
2173
|
+
throw new Error(
|
|
2174
|
+
`apm.yml dependency #${index + 1}: unsupported URL "${urlPart}". Only HTTPS GitHub URLs (https://github.com/owner/repo[.git]) are supported in this version.`
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2177
|
+
return {
|
|
2178
|
+
gitUrl: parsed.gitUrl,
|
|
2179
|
+
owner: parsed.owner,
|
|
2180
|
+
repo: parsed.repo,
|
|
2181
|
+
ref: refPart2 || void 0
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
const [ownerRepo, refPart] = splitOnFirst(trimmed, "#");
|
|
2185
|
+
const slashIndex = ownerRepo.indexOf("/");
|
|
2186
|
+
if (slashIndex === -1 || slashIndex === 0 || slashIndex === ownerRepo.length - 1) {
|
|
2187
|
+
throw new Error(
|
|
2188
|
+
`apm.yml dependency #${index + 1}: shorthand "${entry}" must be in the form "owner/repo[#ref]".`
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
if (ownerRepo.includes("/", slashIndex + 1)) {
|
|
2192
|
+
throw new Error(
|
|
2193
|
+
`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.`
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
const owner = ownerRepo.substring(0, slashIndex).toLowerCase();
|
|
2197
|
+
const repo = ownerRepo.substring(slashIndex + 1).toLowerCase();
|
|
2198
|
+
return {
|
|
2199
|
+
gitUrl: `https://github.com/${owner}/${repo}.git`,
|
|
2200
|
+
owner,
|
|
2201
|
+
repo,
|
|
2202
|
+
ref: refPart || void 0
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
function rejectUnsupportedShorthand(entry, index) {
|
|
2206
|
+
if (entry.startsWith("./") || entry.startsWith("../") || entry.startsWith("/")) {
|
|
2207
|
+
throw new Error(
|
|
2208
|
+
`apm.yml dependency #${index + 1}: local path dependencies ("${entry}") are not yet supported by rulesync.`
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
if (entry.startsWith("git@") || entry.startsWith("ssh://")) {
|
|
2212
|
+
throw new Error(
|
|
2213
|
+
`apm.yml dependency #${index + 1}: SSH URL dependencies ("${entry}") are not yet supported. Use an HTTPS GitHub URL.`
|
|
2214
|
+
);
|
|
2215
|
+
}
|
|
2216
|
+
if (entry.includes("@marketplace")) {
|
|
2217
|
+
throw new Error(
|
|
2218
|
+
`apm.yml dependency #${index + 1}: APM marketplace dependencies ("${entry}") are not yet supported.`
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
function parseHttpsGitHubUrl(url) {
|
|
2223
|
+
let parsed;
|
|
2224
|
+
try {
|
|
2225
|
+
parsed = new URL(url);
|
|
2226
|
+
} catch {
|
|
2227
|
+
return null;
|
|
2228
|
+
}
|
|
2229
|
+
const host = parsed.hostname.toLowerCase();
|
|
2230
|
+
if (host !== "github.com" && host !== "www.github.com") {
|
|
2231
|
+
return null;
|
|
2232
|
+
}
|
|
2233
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
2234
|
+
if (segments.length < 2) {
|
|
2235
|
+
return null;
|
|
2236
|
+
}
|
|
2237
|
+
const rawOwner = segments[0];
|
|
2238
|
+
const rawRepo = segments[1];
|
|
2239
|
+
if (!rawOwner || !rawRepo) {
|
|
2240
|
+
return null;
|
|
2241
|
+
}
|
|
2242
|
+
const owner = rawOwner.toLowerCase();
|
|
2243
|
+
const repo = rawRepo.replace(/\.git$/, "").toLowerCase();
|
|
2244
|
+
return {
|
|
2245
|
+
gitUrl: `https://github.com/${owner}/${repo}.git`,
|
|
2246
|
+
owner,
|
|
2247
|
+
repo
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
function splitOnFirst(input, separator) {
|
|
2251
|
+
const idx = input.indexOf(separator);
|
|
2252
|
+
if (idx === -1) return [input, void 0];
|
|
2253
|
+
return [input.substring(0, idx), input.substring(idx + 1)];
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// src/lib/apm/apm-install.ts
|
|
2257
|
+
var RULESYNC_APM_COMPAT_VERSION = "rulesync-compat/0.1";
|
|
2258
|
+
var APM_PRIMITIVES = [
|
|
2259
|
+
{
|
|
2260
|
+
sourceDir: ".apm/instructions",
|
|
2261
|
+
deployDir: ".github/instructions",
|
|
2262
|
+
packageType: "apm_package"
|
|
2263
|
+
},
|
|
2264
|
+
{
|
|
2265
|
+
sourceDir: ".apm/skills",
|
|
2266
|
+
deployDir: ".github/skills",
|
|
2267
|
+
packageType: "apm_package"
|
|
2268
|
+
}
|
|
2269
|
+
];
|
|
2270
|
+
async function installApm(params) {
|
|
2271
|
+
const { baseDir, options = {}, logger: logger5 } = params;
|
|
2272
|
+
const manifest = await readApmManifest(baseDir);
|
|
2273
|
+
if (manifest.dependencies.length === 0) {
|
|
2274
|
+
logger5.warn("apm.yml has no dependencies.apm entries. Nothing to install.");
|
|
2275
|
+
return { dependenciesProcessed: 0, deployedFileCount: 0, failedDependencyCount: 0 };
|
|
2276
|
+
}
|
|
2277
|
+
const existingLock = await readApmLock(baseDir);
|
|
2278
|
+
if (options.frozen) {
|
|
2279
|
+
if (!existingLock) {
|
|
2280
|
+
throw new Error(
|
|
2281
|
+
"Frozen install failed: rulesync-apm.lock.yaml is missing. Run 'rulesync install --mode apm' to create it."
|
|
2282
|
+
);
|
|
2283
|
+
}
|
|
2284
|
+
const missing = manifest.dependencies.filter(
|
|
2285
|
+
(dep) => !findApmLockDependency(existingLock, canonicalRepoUrl(dep))
|
|
2286
|
+
);
|
|
2287
|
+
if (missing.length > 0) {
|
|
2288
|
+
const names = missing.map((d) => d.gitUrl).join(", ");
|
|
2289
|
+
throw new Error(
|
|
2290
|
+
`Frozen install failed: rulesync-apm.lock.yaml is missing entries for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
const drifted = manifest.dependencies.filter((dep) => {
|
|
2294
|
+
if (dep.ref === void 0) return false;
|
|
2295
|
+
const locked = findApmLockDependency(existingLock, canonicalRepoUrl(dep));
|
|
2296
|
+
return locked?.resolved_ref !== void 0 && locked.resolved_ref !== dep.ref;
|
|
2297
|
+
});
|
|
2298
|
+
if (drifted.length > 0) {
|
|
2299
|
+
const names = drifted.map((d) => {
|
|
2300
|
+
const locked = findApmLockDependency(existingLock, canonicalRepoUrl(d));
|
|
2301
|
+
return `${d.gitUrl} (manifest=${d.ref}, lock=${locked?.resolved_ref})`;
|
|
2302
|
+
}).join(", ");
|
|
2303
|
+
throw new Error(
|
|
2304
|
+
`Frozen install failed: manifest ref does not match rulesync-apm.lock.yaml for: ${names}. Run 'rulesync install --mode apm' to update the lockfile.`
|
|
2305
|
+
);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
const token = GitHubClient.resolveToken(options.token);
|
|
2309
|
+
const client = new GitHubClient({ token });
|
|
2310
|
+
const semaphore = new Semaphore2(FETCH_CONCURRENCY_LIMIT);
|
|
2311
|
+
const newLock = createEmptyApmLock({
|
|
2312
|
+
apmVersion: existingLock?.apm_version ?? RULESYNC_APM_COMPAT_VERSION,
|
|
2313
|
+
existingLock
|
|
2314
|
+
});
|
|
2315
|
+
const frozen = options.frozen ?? false;
|
|
2316
|
+
const runOne = async (dep) => {
|
|
2317
|
+
const installed = await installDependency({
|
|
2318
|
+
dep,
|
|
2319
|
+
client,
|
|
2320
|
+
semaphore,
|
|
2321
|
+
baseDir,
|
|
2322
|
+
existingLock,
|
|
2323
|
+
frozen,
|
|
2324
|
+
update: options.update ?? false,
|
|
2325
|
+
logger: logger5
|
|
2326
|
+
});
|
|
2327
|
+
return {
|
|
2328
|
+
status: "ok",
|
|
2329
|
+
lockEntry: installed.lockEntry,
|
|
2330
|
+
deployedCount: installed.deployedFiles.length
|
|
2331
|
+
};
|
|
2332
|
+
};
|
|
2333
|
+
const results = frozen ? await Promise.all(manifest.dependencies.map(runOne)) : await Promise.all(
|
|
2334
|
+
manifest.dependencies.map(async (dep) => {
|
|
2335
|
+
try {
|
|
2336
|
+
return await runOne(dep);
|
|
2337
|
+
} catch (error) {
|
|
2338
|
+
logger5.error(`Failed to install apm dependency "${dep.gitUrl}": ${formatError(error)}`);
|
|
2339
|
+
if (error instanceof GitHubClientError) {
|
|
2340
|
+
logGitHubAuthHints({ error, logger: logger5 });
|
|
2341
|
+
}
|
|
2342
|
+
const previous = existingLock ? findApmLockDependency(existingLock, canonicalRepoUrl(dep)) : void 0;
|
|
2343
|
+
return { status: "failed", previous };
|
|
2344
|
+
}
|
|
2345
|
+
})
|
|
2346
|
+
);
|
|
2347
|
+
let totalDeployed = 0;
|
|
2348
|
+
let failedCount = 0;
|
|
2349
|
+
for (const result of results) {
|
|
2350
|
+
if (result.status === "ok") {
|
|
2351
|
+
newLock.dependencies.push(result.lockEntry);
|
|
2352
|
+
totalDeployed += result.deployedCount;
|
|
2353
|
+
} else {
|
|
2354
|
+
failedCount += 1;
|
|
2355
|
+
if (result.previous) {
|
|
2356
|
+
newLock.dependencies.push(result.previous);
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
if (existingLock) {
|
|
2361
|
+
const newDeployedFiles = new Set(newLock.dependencies.flatMap((d) => d.deployed_files));
|
|
2362
|
+
const toDelete = [];
|
|
2363
|
+
for (const prev of existingLock.dependencies) {
|
|
2364
|
+
for (const deployed of prev.deployed_files) {
|
|
2365
|
+
if (!newDeployedFiles.has(deployed)) {
|
|
2366
|
+
toDelete.push(deployed);
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
for (const relativePath of toDelete) {
|
|
2371
|
+
if (posix2.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
|
|
2372
|
+
logger5.warn(`Refusing to remove stale apm file with suspicious path: "${relativePath}".`);
|
|
2373
|
+
continue;
|
|
2374
|
+
}
|
|
2375
|
+
try {
|
|
2376
|
+
checkPathTraversal({ relativePath, intendedRootDir: baseDir });
|
|
2377
|
+
} catch {
|
|
2378
|
+
logger5.warn(`Refusing to remove stale apm file outside baseDir: "${relativePath}".`);
|
|
2379
|
+
continue;
|
|
2380
|
+
}
|
|
2381
|
+
const absolute = join6(baseDir, relativePath);
|
|
2382
|
+
await removeFile(absolute);
|
|
2383
|
+
logger5.debug(`Removed stale apm file: ${relativePath}`);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
if (!frozen) {
|
|
2387
|
+
newLock.generated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2388
|
+
await writeApmLock({ baseDir, lock: newLock });
|
|
2389
|
+
if (failedCount === 0) {
|
|
2390
|
+
logger5.debug("rulesync-apm.lock.yaml updated.");
|
|
2391
|
+
} else {
|
|
2392
|
+
logger5.warn(
|
|
2393
|
+
`rulesync-apm.lock.yaml written with partially successful installs (${failedCount} dep(s) failed).`
|
|
2394
|
+
);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
return {
|
|
2398
|
+
dependenciesProcessed: manifest.dependencies.length,
|
|
2399
|
+
deployedFileCount: totalDeployed,
|
|
2400
|
+
failedDependencyCount: failedCount
|
|
2401
|
+
};
|
|
2402
|
+
}
|
|
2403
|
+
async function installDependency(params) {
|
|
2404
|
+
const { dep, client, semaphore, baseDir, existingLock, frozen, update, logger: logger5 } = params;
|
|
2405
|
+
const repoUrl = canonicalRepoUrl(dep);
|
|
2406
|
+
const locked = existingLock ? findApmLockDependency(existingLock, repoUrl) : void 0;
|
|
2407
|
+
let resolvedRef;
|
|
2408
|
+
let resolvedSha;
|
|
2409
|
+
if (locked && !update && locked.resolved_commit && locked.resolved_ref) {
|
|
2410
|
+
resolvedRef = locked.resolved_ref;
|
|
2411
|
+
resolvedSha = locked.resolved_commit;
|
|
2412
|
+
logger5.debug(`Using locked commit for ${repoUrl}: ${resolvedSha}`);
|
|
2413
|
+
} else {
|
|
2414
|
+
resolvedRef = dep.ref ?? await client.getDefaultBranch(dep.owner, dep.repo);
|
|
2415
|
+
resolvedSha = await client.resolveRefToSha(dep.owner, dep.repo, resolvedRef);
|
|
2416
|
+
logger5.debug(`Resolved ${repoUrl} ref "${resolvedRef}" -> ${resolvedSha}`);
|
|
2417
|
+
}
|
|
2418
|
+
const deployed = [];
|
|
2419
|
+
for (const primitive of APM_PRIMITIVES) {
|
|
2420
|
+
const remoteBase = dep.path ? toPosixPath(posix2.join(dep.path, primitive.sourceDir)) : primitive.sourceDir;
|
|
2421
|
+
const files = await listPrimitiveFiles({
|
|
2422
|
+
client,
|
|
2423
|
+
semaphore,
|
|
2424
|
+
owner: dep.owner,
|
|
2425
|
+
repo: dep.repo,
|
|
2426
|
+
ref: resolvedSha,
|
|
2427
|
+
remoteBase,
|
|
2428
|
+
logger: logger5
|
|
2429
|
+
});
|
|
2430
|
+
if (files.length === 0) continue;
|
|
2431
|
+
for (const file of files) {
|
|
2432
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
2433
|
+
logger5.warn(
|
|
2434
|
+
`Skipping "${file.path}" from ${repoUrl}: ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
2435
|
+
);
|
|
2436
|
+
continue;
|
|
2437
|
+
}
|
|
2438
|
+
const relativeToBase = posix2.relative(remoteBase, toPosixPath(file.path));
|
|
2439
|
+
if (!relativeToBase || relativeToBase.startsWith("..") || posix2.isAbsolute(relativeToBase)) {
|
|
2440
|
+
logger5.warn(
|
|
2441
|
+
`Skipping "${file.path}" from ${repoUrl}: resolved outside of "${remoteBase}".`
|
|
2442
|
+
);
|
|
2443
|
+
continue;
|
|
2444
|
+
}
|
|
2445
|
+
const deployRelative = toPosixPath(join6(primitive.deployDir, relativeToBase));
|
|
2446
|
+
checkPathTraversal({
|
|
2447
|
+
relativePath: deployRelative,
|
|
2448
|
+
intendedRootDir: baseDir
|
|
2449
|
+
});
|
|
2450
|
+
const content = await withSemaphore(
|
|
2451
|
+
semaphore,
|
|
2452
|
+
() => client.getFileContent(dep.owner, dep.repo, file.path, resolvedSha)
|
|
2453
|
+
);
|
|
2454
|
+
const byteLength = Buffer.byteLength(content, "utf8");
|
|
2455
|
+
if (byteLength > MAX_FILE_SIZE) {
|
|
2456
|
+
logger5.warn(
|
|
2457
|
+
`Skipping "${file.path}" from ${repoUrl}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
2458
|
+
);
|
|
2459
|
+
continue;
|
|
2460
|
+
}
|
|
2461
|
+
deployed.push({ path: deployRelative, content });
|
|
2462
|
+
if (!frozen) {
|
|
2463
|
+
await writeFileContent(join6(baseDir, deployRelative), content);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
deployed.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
2468
|
+
const deployedFiles = deployed.map((d) => d.path);
|
|
2469
|
+
const contentHash = computeContentHash(deployed);
|
|
2470
|
+
if (frozen && locked?.content_hash) {
|
|
2471
|
+
if (RULESYNC_CONTENT_HASH_REGEX.test(locked.content_hash)) {
|
|
2472
|
+
if (locked.content_hash !== contentHash) {
|
|
2473
|
+
throw new Error(
|
|
2474
|
+
`content_hash mismatch for ${repoUrl}: lock=${locked.content_hash} computed=${contentHash}. Refuse to trust the deployment under --frozen.`
|
|
2475
|
+
);
|
|
2476
|
+
}
|
|
2477
|
+
} else {
|
|
2478
|
+
logger5.debug(
|
|
2479
|
+
`Skipping content_hash integrity check for ${repoUrl}: recorded hash "${locked.content_hash}" was not written by rulesync.`
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
if (frozen) {
|
|
2484
|
+
for (const { path: deployRelative, content } of deployed) {
|
|
2485
|
+
await writeFileContent(join6(baseDir, deployRelative), content);
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
const lockEntry = {
|
|
2489
|
+
repo_url: repoUrl,
|
|
2490
|
+
resolved_commit: resolvedSha,
|
|
2491
|
+
resolved_ref: resolvedRef,
|
|
2492
|
+
depth: 1,
|
|
2493
|
+
package_type: "apm_package",
|
|
2494
|
+
content_hash: contentHash,
|
|
2495
|
+
deployed_files: deployedFiles
|
|
2496
|
+
};
|
|
2497
|
+
if (dep.path) {
|
|
2498
|
+
lockEntry.virtual_path = dep.path;
|
|
2499
|
+
}
|
|
2500
|
+
logger5.info(`Installed ${deployedFiles.length} file(s) from ${repoUrl}@${shortSha(resolvedSha)}`);
|
|
2501
|
+
return { lockEntry, deployedFiles };
|
|
2502
|
+
}
|
|
2503
|
+
function computeContentHash(files) {
|
|
2504
|
+
const hash = createHash("sha256");
|
|
2505
|
+
for (const { path: path2, content } of files) {
|
|
2506
|
+
hash.update(path2);
|
|
2507
|
+
hash.update("\0");
|
|
2508
|
+
hash.update(content);
|
|
2509
|
+
hash.update("\0");
|
|
2510
|
+
}
|
|
2511
|
+
return `sha256:${hash.digest("hex")}`;
|
|
2512
|
+
}
|
|
2513
|
+
async function listPrimitiveFiles(params) {
|
|
2514
|
+
const { client, semaphore, owner, repo, ref, remoteBase, logger: logger5 } = params;
|
|
2515
|
+
try {
|
|
2516
|
+
return await listDirectoryRecursive({
|
|
2517
|
+
client,
|
|
2518
|
+
owner,
|
|
2519
|
+
repo,
|
|
2520
|
+
path: remoteBase,
|
|
2521
|
+
ref,
|
|
2522
|
+
semaphore
|
|
2523
|
+
});
|
|
2524
|
+
} catch (error) {
|
|
2525
|
+
if (error instanceof GitHubClientError && error.statusCode === 404) {
|
|
2526
|
+
logger5.debug(`No ${remoteBase}/ in ${owner}/${repo}, skipping.`);
|
|
2527
|
+
return [];
|
|
2528
|
+
}
|
|
2529
|
+
throw error;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
function canonicalRepoUrl(dep) {
|
|
2533
|
+
return `https://github.com/${dep.owner}/${dep.repo}`;
|
|
2534
|
+
}
|
|
2535
|
+
function shortSha(sha) {
|
|
2536
|
+
return sha.substring(0, 7);
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// src/lib/gh/gh-install.ts
|
|
2540
|
+
import { createHash as createHash2 } from "crypto";
|
|
2541
|
+
import { basename, join as join9, posix as posix3 } from "path";
|
|
2542
|
+
import { Semaphore as Semaphore3 } from "es-toolkit/promise";
|
|
2543
|
+
|
|
2544
|
+
// src/lib/gh/gh-frontmatter.ts
|
|
2545
|
+
import { dump as dump3, load as load3 } from "js-yaml";
|
|
2546
|
+
var FRONTMATTER_FENCE = "---";
|
|
2547
|
+
function injectSourceMetadata(params) {
|
|
2548
|
+
const { content, source, repository, ref } = params;
|
|
2549
|
+
const provenance = { source, repository, ref };
|
|
2550
|
+
let openFenceLen;
|
|
2551
|
+
if (content.startsWith(`${FRONTMATTER_FENCE}\r
|
|
2552
|
+
`)) {
|
|
2553
|
+
openFenceLen = 5;
|
|
2554
|
+
} else if (content.startsWith(`${FRONTMATTER_FENCE}
|
|
2555
|
+
`)) {
|
|
2556
|
+
openFenceLen = 4;
|
|
2557
|
+
} else if (content === FRONTMATTER_FENCE) {
|
|
2558
|
+
openFenceLen = 3;
|
|
2559
|
+
} else {
|
|
2560
|
+
const yaml2 = dump3(provenance, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2561
|
+
return `${FRONTMATTER_FENCE}
|
|
2562
|
+
${yaml2}${FRONTMATTER_FENCE}
|
|
2563
|
+
${content}`;
|
|
2564
|
+
}
|
|
2565
|
+
const afterOpen = content.substring(openFenceLen);
|
|
2566
|
+
let fmBody;
|
|
2567
|
+
let rest;
|
|
2568
|
+
if (afterOpen.startsWith("---\n") || afterOpen.startsWith("---\r\n") || afterOpen === "---") {
|
|
2569
|
+
fmBody = "";
|
|
2570
|
+
const fenceLen = afterOpen.startsWith("---\r\n") ? 5 : afterOpen === "---" ? 3 : 4;
|
|
2571
|
+
rest = afterOpen.substring(fenceLen);
|
|
2572
|
+
} else {
|
|
2573
|
+
const match = /\n---(\r?\n|$)/.exec(afterOpen);
|
|
2574
|
+
if (!match) {
|
|
2575
|
+
throw new Error("invalid frontmatter");
|
|
2576
|
+
}
|
|
2577
|
+
fmBody = afterOpen.substring(0, match.index);
|
|
2578
|
+
rest = afterOpen.substring(match.index + match[0].length);
|
|
2579
|
+
}
|
|
2580
|
+
let loaded;
|
|
2581
|
+
try {
|
|
2582
|
+
loaded = load3(fmBody);
|
|
2583
|
+
} catch {
|
|
2584
|
+
throw new Error("invalid frontmatter");
|
|
2585
|
+
}
|
|
2586
|
+
if (loaded === null || loaded === void 0) {
|
|
2587
|
+
const yaml2 = dump3(provenance, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2588
|
+
return `${FRONTMATTER_FENCE}
|
|
2589
|
+
${yaml2}${FRONTMATTER_FENCE}
|
|
2590
|
+
${rest}`;
|
|
2591
|
+
}
|
|
2592
|
+
if (typeof loaded !== "object" || Array.isArray(loaded)) {
|
|
2593
|
+
throw new Error("invalid frontmatter");
|
|
2594
|
+
}
|
|
2595
|
+
const existing = loaded;
|
|
2596
|
+
const merged = {
|
|
2597
|
+
...existing,
|
|
2598
|
+
...provenance
|
|
2599
|
+
};
|
|
2600
|
+
const yaml = dump3(merged, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2601
|
+
return `${FRONTMATTER_FENCE}
|
|
2602
|
+
${yaml}${FRONTMATTER_FENCE}
|
|
2603
|
+
${rest}`;
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
// src/lib/gh/gh-lock.ts
|
|
2607
|
+
import { join as join7 } from "path";
|
|
2608
|
+
import { dump as dump4, load as load4 } from "js-yaml";
|
|
2609
|
+
import { optional as optional3, refine as refine2, z as z6 } from "zod/mini";
|
|
2610
|
+
var GH_LOCKFILE_FILE_NAME = "rulesync-gh.lock.yaml";
|
|
2611
|
+
var GH_LOCKFILE_VERSION = "1";
|
|
2612
|
+
var RULESYNC_CONTENT_HASH_REGEX2 = /^sha256:[0-9a-f]{64}$/;
|
|
2613
|
+
var ScopeSchema = z6.enum(["project", "user"]);
|
|
2614
|
+
var GhLockInstallationSchema = z6.looseObject({
|
|
2615
|
+
source: z6.string(),
|
|
2616
|
+
owner: z6.string(),
|
|
2617
|
+
repo: z6.string(),
|
|
2618
|
+
agent: z6.string(),
|
|
2619
|
+
scope: ScopeSchema,
|
|
2620
|
+
skill: z6.string(),
|
|
2621
|
+
requested_ref: optional3(z6.string()),
|
|
2622
|
+
resolved_ref: z6.string(),
|
|
2623
|
+
resolved_commit: z6.string().check(refine2((v) => /^[0-9a-f]{40}$/.test(v), "resolved_commit must be a 40-char hex SHA")),
|
|
2624
|
+
install_dir: z6.string(),
|
|
2625
|
+
deployed_files: z6.array(z6.string()),
|
|
2626
|
+
content_hash: optional3(z6.string())
|
|
2627
|
+
});
|
|
2628
|
+
var GhLockSchema = z6.looseObject({
|
|
2629
|
+
lockfile_version: z6.literal("1"),
|
|
2630
|
+
generated_at: z6.string(),
|
|
2631
|
+
installations: z6.array(GhLockInstallationSchema)
|
|
2632
|
+
});
|
|
2633
|
+
function getGhLockPath(baseDir) {
|
|
2634
|
+
return join7(baseDir, GH_LOCKFILE_FILE_NAME);
|
|
2635
|
+
}
|
|
2636
|
+
function createEmptyGhLock(params) {
|
|
2637
|
+
const base = params?.existingLock ? { ...params.existingLock } : {};
|
|
2638
|
+
return {
|
|
2639
|
+
...base,
|
|
2640
|
+
lockfile_version: GH_LOCKFILE_VERSION,
|
|
2641
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2642
|
+
installations: []
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
function parseGhLock(content) {
|
|
2646
|
+
if (!content.trim()) {
|
|
2647
|
+
return null;
|
|
2648
|
+
}
|
|
2649
|
+
let loaded;
|
|
2650
|
+
try {
|
|
2651
|
+
loaded = load4(content);
|
|
2652
|
+
} catch {
|
|
2653
|
+
return null;
|
|
2654
|
+
}
|
|
2655
|
+
if (!loaded || typeof loaded !== "object") {
|
|
2656
|
+
return null;
|
|
2657
|
+
}
|
|
2658
|
+
const parsed = GhLockSchema.safeParse(loaded);
|
|
2659
|
+
if (!parsed.success) {
|
|
2660
|
+
const issues = parsed.error.issues.map((issue) => ` - ${issue.path.join(".") || "<root>"}: ${issue.message}`).join("\n");
|
|
2661
|
+
throw new Error(`Invalid ${GH_LOCKFILE_FILE_NAME}:
|
|
2662
|
+
${issues}`);
|
|
2663
|
+
}
|
|
2664
|
+
return parsed.data;
|
|
2665
|
+
}
|
|
2666
|
+
async function readGhLock(baseDir) {
|
|
2667
|
+
const path2 = getGhLockPath(baseDir);
|
|
2668
|
+
if (!await fileExists(path2)) {
|
|
2669
|
+
return null;
|
|
2670
|
+
}
|
|
2671
|
+
const content = await readFileContent(path2);
|
|
2672
|
+
return parseGhLock(content);
|
|
2673
|
+
}
|
|
2674
|
+
async function writeGhLock(params) {
|
|
2675
|
+
const path2 = getGhLockPath(params.baseDir);
|
|
2676
|
+
const content = serializeGhLock(params.lock);
|
|
2677
|
+
await writeFileContent(path2, content);
|
|
2678
|
+
}
|
|
2679
|
+
function serializeGhLock(lock) {
|
|
2680
|
+
return dump4(lock, { noRefs: true, lineWidth: -1, sortKeys: false });
|
|
2681
|
+
}
|
|
2682
|
+
function findGhLockInstallation(lock, params) {
|
|
2683
|
+
const target = params.source.toLowerCase();
|
|
2684
|
+
return lock.installations.find(
|
|
2685
|
+
(i) => i.source.toLowerCase() === target && i.agent === params.agent && i.scope === params.scope && i.skill === params.skill
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
// src/lib/gh/gh-paths.ts
|
|
2690
|
+
import { join as join8 } from "path";
|
|
2691
|
+
var GH_AGENTS = [
|
|
2692
|
+
"github-copilot",
|
|
2693
|
+
"claude-code",
|
|
2694
|
+
"cursor",
|
|
2695
|
+
"codex",
|
|
2696
|
+
"gemini",
|
|
2697
|
+
"antigravity"
|
|
2698
|
+
];
|
|
2699
|
+
function relativeInstallDirFor(params) {
|
|
2700
|
+
const { agent, scope } = params;
|
|
2701
|
+
if (scope === "project") {
|
|
2702
|
+
if (agent === "claude-code") {
|
|
2703
|
+
return join8(".claude", "skills");
|
|
2704
|
+
}
|
|
2705
|
+
return join8(".agents", "skills");
|
|
2706
|
+
}
|
|
2707
|
+
switch (agent) {
|
|
2708
|
+
case "github-copilot":
|
|
2709
|
+
return join8(".copilot", "skills");
|
|
2710
|
+
case "claude-code":
|
|
2711
|
+
return join8(".claude", "skills");
|
|
2712
|
+
case "cursor":
|
|
2713
|
+
return join8(".cursor", "skills");
|
|
2714
|
+
case "codex":
|
|
2715
|
+
return join8(".codex", "skills");
|
|
2716
|
+
case "gemini":
|
|
2717
|
+
return join8(".gemini", "skills");
|
|
2718
|
+
case "antigravity":
|
|
2719
|
+
return join8(".gemini", "antigravity", "skills");
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
// src/lib/gh/gh-install.ts
|
|
2724
|
+
var SKILLS_REMOTE_DIR = "skills";
|
|
2725
|
+
var SKILL_FILE_NAME2 = "SKILL.md";
|
|
2726
|
+
async function installGh(params) {
|
|
2727
|
+
const { baseDir, sources, options = {}, logger: logger5 } = params;
|
|
2728
|
+
if (sources.length === 0) {
|
|
2729
|
+
return { sourcesProcessed: 0, installedSkillCount: 0, failedSourceCount: 0 };
|
|
2730
|
+
}
|
|
2731
|
+
const resolvedSources = sources.map((entry) => {
|
|
2732
|
+
const parsed = parseSource(entry.source);
|
|
2733
|
+
if (parsed.provider !== "github") {
|
|
2734
|
+
throw new Error(
|
|
2735
|
+
`--mode gh only supports GitHub sources. "${entry.source}" resolves to provider "${parsed.provider}".`
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
if (entry.transport !== void 0 && entry.transport !== "github") {
|
|
2739
|
+
throw new Error(
|
|
2740
|
+
`--mode gh: field "transport" is not supported (got "${entry.transport}" for source "${entry.source}"). Drop the field or switch to --mode rulesync.`
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
if (entry.path !== void 0) {
|
|
2744
|
+
throw new Error(
|
|
2745
|
+
`--mode gh: field "path" is not supported for source "${entry.source}". The remote layout is fixed to "skills/<name>/SKILL.md".`
|
|
2746
|
+
);
|
|
2747
|
+
}
|
|
2748
|
+
const agent = entry.agent ?? "github-copilot";
|
|
2749
|
+
if (!GH_AGENTS.includes(agent)) {
|
|
2750
|
+
throw new Error(
|
|
2751
|
+
`--mode gh: unknown agent "${agent}" for source "${entry.source}". Valid agents: ${GH_AGENTS.join(", ")}.`
|
|
2752
|
+
);
|
|
2753
|
+
}
|
|
2754
|
+
const scope = entry.scope ?? "project";
|
|
2755
|
+
return {
|
|
2756
|
+
entry,
|
|
2757
|
+
owner: parsed.owner,
|
|
2758
|
+
repo: parsed.repo,
|
|
2759
|
+
ref: entry.ref ?? parsed.ref,
|
|
2760
|
+
agent,
|
|
2761
|
+
scope
|
|
2762
|
+
};
|
|
2763
|
+
});
|
|
2764
|
+
const existingLock = await readGhLock(baseDir);
|
|
2765
|
+
const frozen = options.frozen ?? false;
|
|
2766
|
+
const update = options.update ?? false;
|
|
2767
|
+
if (frozen && !existingLock) {
|
|
2768
|
+
throw new Error(
|
|
2769
|
+
"Frozen install failed: rulesync-gh.lock.yaml is missing. Run 'rulesync install --mode gh' to create it."
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
if (frozen && existingLock) {
|
|
2773
|
+
const uncovered = [];
|
|
2774
|
+
for (const rs of resolvedSources) {
|
|
2775
|
+
const hasAny = existingLock.installations.some(
|
|
2776
|
+
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase() && i.agent === rs.agent && i.scope === rs.scope
|
|
2777
|
+
);
|
|
2778
|
+
if (!hasAny) {
|
|
2779
|
+
uncovered.push(`${rs.entry.source} (agent=${rs.agent}, scope=${rs.scope})`);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
if (uncovered.length > 0) {
|
|
2783
|
+
throw new Error(
|
|
2784
|
+
`Frozen install failed: rulesync-gh.lock.yaml is missing entries for: ${uncovered.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
2785
|
+
);
|
|
2786
|
+
}
|
|
2787
|
+
const drifted = [];
|
|
2788
|
+
for (const rs of resolvedSources) {
|
|
2789
|
+
if (!rs.ref) continue;
|
|
2790
|
+
const matches = existingLock.installations.filter(
|
|
2791
|
+
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase()
|
|
2792
|
+
);
|
|
2793
|
+
for (const m of matches) {
|
|
2794
|
+
if (m.requested_ref !== void 0 && m.requested_ref !== rs.ref) {
|
|
2795
|
+
drifted.push(`${rs.entry.source} (manifest=${rs.ref}, lock=${m.requested_ref})`);
|
|
2796
|
+
break;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
if (drifted.length > 0) {
|
|
2801
|
+
throw new Error(
|
|
2802
|
+
`Frozen install failed: manifest ref does not match rulesync-gh.lock.yaml for: ${drifted.join(", ")}. Run 'rulesync install --mode gh' to update the lockfile.`
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
const token = GitHubClient.resolveToken(options.token);
|
|
2807
|
+
const client = new GitHubClient({ token });
|
|
2808
|
+
const semaphore = new Semaphore3(FETCH_CONCURRENCY_LIMIT);
|
|
2809
|
+
const newLock = createEmptyGhLock({ existingLock });
|
|
2810
|
+
const runOne = async (rs) => {
|
|
2811
|
+
const installations = await installSource({
|
|
2812
|
+
rs,
|
|
2813
|
+
client,
|
|
2814
|
+
semaphore,
|
|
2815
|
+
baseDir,
|
|
2816
|
+
existingLock,
|
|
2817
|
+
frozen,
|
|
2818
|
+
update,
|
|
2819
|
+
logger: logger5
|
|
2820
|
+
});
|
|
2821
|
+
return { status: "ok", installations };
|
|
2822
|
+
};
|
|
2823
|
+
const results = frozen ? await Promise.all(resolvedSources.map(runOne)) : await Promise.all(
|
|
2824
|
+
resolvedSources.map(async (rs) => {
|
|
2825
|
+
try {
|
|
2826
|
+
return await runOne(rs);
|
|
2827
|
+
} catch (error) {
|
|
2828
|
+
logger5.error(`Failed to install gh source "${rs.entry.source}": ${formatError(error)}`);
|
|
2829
|
+
if (error instanceof GitHubClientError) {
|
|
2830
|
+
logGitHubAuthHints({ error, logger: logger5 });
|
|
2831
|
+
}
|
|
2832
|
+
const preserved = existingLock ? existingLock.installations.filter(
|
|
2833
|
+
(i) => i.source.toLowerCase() === rs.entry.source.toLowerCase()
|
|
2834
|
+
) : [];
|
|
2835
|
+
return { status: "failed", preserved };
|
|
2836
|
+
}
|
|
2837
|
+
})
|
|
2838
|
+
);
|
|
2839
|
+
if (frozen) {
|
|
2840
|
+
for (const result of results) {
|
|
2841
|
+
if (result.status !== "ok") continue;
|
|
2842
|
+
for (const inst of result.installations) {
|
|
2843
|
+
for (const d of inst.deployed) {
|
|
2844
|
+
await writeFileContent(d.absolutePath, d.content);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
let totalInstalled = 0;
|
|
2850
|
+
let failedCount = 0;
|
|
2851
|
+
for (const result of results) {
|
|
2852
|
+
if (result.status === "ok") {
|
|
2853
|
+
for (const inst of result.installations) {
|
|
2854
|
+
newLock.installations.push(inst.installation);
|
|
2855
|
+
}
|
|
2856
|
+
totalInstalled += result.installations.length;
|
|
2857
|
+
} else {
|
|
2858
|
+
failedCount += 1;
|
|
2859
|
+
for (const preserved of result.preserved) {
|
|
2860
|
+
newLock.installations.push(preserved);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
if (existingLock) {
|
|
2865
|
+
const newDeployed = /* @__PURE__ */ new Set();
|
|
2866
|
+
for (const inst of newLock.installations) {
|
|
2867
|
+
for (const file of inst.deployed_files) {
|
|
2868
|
+
newDeployed.add(`${inst.scope}::${file}`);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
for (const prev of existingLock.installations) {
|
|
2872
|
+
for (const deployed of prev.deployed_files) {
|
|
2873
|
+
const key = `${prev.scope}::${deployed}`;
|
|
2874
|
+
if (newDeployed.has(key)) continue;
|
|
2875
|
+
await removeStaleFile({
|
|
2876
|
+
relativePath: deployed,
|
|
2877
|
+
scope: prev.scope === "user" ? "user" : "project",
|
|
2878
|
+
baseDir,
|
|
2879
|
+
logger: logger5
|
|
2880
|
+
});
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
if (!frozen) {
|
|
2885
|
+
newLock.generated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2886
|
+
await writeGhLock({ baseDir, lock: newLock });
|
|
2887
|
+
if (failedCount === 0) {
|
|
2888
|
+
logger5.debug("rulesync-gh.lock.yaml updated.");
|
|
2889
|
+
} else {
|
|
2890
|
+
logger5.warn(
|
|
2891
|
+
`rulesync-gh.lock.yaml written with partially successful installs (${failedCount} source(s) failed).`
|
|
2892
|
+
);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
return {
|
|
2896
|
+
sourcesProcessed: sources.length,
|
|
2897
|
+
installedSkillCount: totalInstalled,
|
|
2898
|
+
failedSourceCount: failedCount
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
async function installSource(params) {
|
|
2902
|
+
const { rs, client, semaphore, baseDir, existingLock, frozen, update, logger: logger5 } = params;
|
|
2903
|
+
const { entry, owner, repo, agent, scope } = rs;
|
|
2904
|
+
const sourceKey = entry.source;
|
|
2905
|
+
let resolvedRef;
|
|
2906
|
+
let usedTag = false;
|
|
2907
|
+
if (rs.ref) {
|
|
2908
|
+
resolvedRef = rs.ref;
|
|
2909
|
+
} else {
|
|
2910
|
+
try {
|
|
2911
|
+
const release = await client.getLatestRelease(owner, repo);
|
|
2912
|
+
resolvedRef = release.tag_name;
|
|
2913
|
+
usedTag = true;
|
|
2914
|
+
} catch (error) {
|
|
2915
|
+
if (is404(error)) {
|
|
2916
|
+
resolvedRef = await client.getDefaultBranch(owner, repo);
|
|
2917
|
+
} else {
|
|
2918
|
+
throw error;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
const resolvedSha = await client.resolveRefToSha(owner, repo, resolvedRef);
|
|
2923
|
+
logger5.debug(`Resolved ${sourceKey} -> ref=${resolvedRef} sha=${resolvedSha}`);
|
|
2924
|
+
let topLevel;
|
|
2925
|
+
try {
|
|
2926
|
+
topLevel = await client.listDirectory(owner, repo, SKILLS_REMOTE_DIR, resolvedSha);
|
|
2927
|
+
} catch (error) {
|
|
2928
|
+
if (is404(error)) {
|
|
2929
|
+
logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
|
|
2930
|
+
return [];
|
|
2931
|
+
}
|
|
2932
|
+
throw error;
|
|
2933
|
+
}
|
|
2934
|
+
const skillDirs = topLevel.filter((e) => e.type === "dir").map((e) => ({ name: e.name, path: e.path }));
|
|
2935
|
+
const validatedSkills = [];
|
|
2936
|
+
for (const sk of skillDirs) {
|
|
2937
|
+
const info = await withSemaphore(
|
|
2938
|
+
semaphore,
|
|
2939
|
+
() => client.getFileInfo(owner, repo, posix3.join(sk.path, SKILL_FILE_NAME2), resolvedSha)
|
|
2940
|
+
);
|
|
2941
|
+
if (info) {
|
|
2942
|
+
validatedSkills.push(sk);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
let selected = validatedSkills;
|
|
2946
|
+
if (entry.skills && entry.skills.length > 0) {
|
|
2947
|
+
const requested = new Set(entry.skills);
|
|
2948
|
+
selected = validatedSkills.filter((s) => requested.has(s.name));
|
|
2949
|
+
const presentNames = new Set(validatedSkills.map((s) => s.name));
|
|
2950
|
+
for (const want of entry.skills) {
|
|
2951
|
+
if (!presentNames.has(want)) {
|
|
2952
|
+
logger5.warn(`Requested skill "${want}" not found in ${sourceKey} under skills/. Skipping.`);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
if (frozen && existingLock) {
|
|
2957
|
+
const missing = [];
|
|
2958
|
+
for (const sk of selected) {
|
|
2959
|
+
const locked = findGhLockInstallation(existingLock, {
|
|
2960
|
+
source: sourceKey,
|
|
2961
|
+
agent,
|
|
2962
|
+
scope,
|
|
2963
|
+
skill: sk.name
|
|
2964
|
+
});
|
|
2965
|
+
if (!locked) {
|
|
2966
|
+
missing.push(sk.name);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
if (missing.length > 0) {
|
|
2970
|
+
throw new Error(
|
|
2971
|
+
`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.`
|
|
2972
|
+
);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
const results = [];
|
|
2976
|
+
const installRelDir = relativeInstallDirFor({ agent, scope });
|
|
2977
|
+
const scopeRoot = scope === "user" ? getHomeDirectory() : baseDir;
|
|
2978
|
+
const sourceUrl = `https://github.com/${owner}/${repo}`;
|
|
2979
|
+
const repository = `${owner}/${repo}`;
|
|
2980
|
+
const provenanceRef = usedTag ? resolvedRef : resolvedSha;
|
|
2981
|
+
for (const sk of selected) {
|
|
2982
|
+
const locked = existingLock && !update ? findGhLockInstallation(existingLock, {
|
|
2983
|
+
source: sourceKey,
|
|
2984
|
+
agent,
|
|
2985
|
+
scope,
|
|
2986
|
+
skill: sk.name
|
|
2987
|
+
}) : void 0;
|
|
2988
|
+
const allFiles = await listDirectoryRecursive({
|
|
2989
|
+
client,
|
|
2990
|
+
owner,
|
|
2991
|
+
repo,
|
|
2992
|
+
path: sk.path,
|
|
2993
|
+
ref: resolvedSha,
|
|
2994
|
+
semaphore
|
|
2995
|
+
});
|
|
2996
|
+
const deployed = [];
|
|
2997
|
+
for (const file of allFiles) {
|
|
2998
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
2999
|
+
logger5.warn(
|
|
3000
|
+
`Skipping "${file.path}" from ${sourceKey}: ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
3001
|
+
);
|
|
3002
|
+
continue;
|
|
3003
|
+
}
|
|
3004
|
+
const relativeToSkill = posix3.relative(sk.path, toPosixPath(file.path));
|
|
3005
|
+
if (!relativeToSkill || relativeToSkill.startsWith("..") || posix3.isAbsolute(relativeToSkill)) {
|
|
3006
|
+
logger5.warn(`Skipping "${file.path}" from ${sourceKey}: resolved outside of "${sk.path}".`);
|
|
3007
|
+
continue;
|
|
3008
|
+
}
|
|
3009
|
+
const deployRelative = toPosixPath(join9(installRelDir, sk.name, relativeToSkill));
|
|
3010
|
+
checkPathTraversal({ relativePath: deployRelative, intendedRootDir: scopeRoot });
|
|
3011
|
+
const installAbs = join9(scopeRoot, installRelDir);
|
|
3012
|
+
const withinInstallDir = toPosixPath(join9(sk.name, relativeToSkill));
|
|
3013
|
+
checkPathTraversal({ relativePath: withinInstallDir, intendedRootDir: installAbs });
|
|
3014
|
+
let content = await withSemaphore(
|
|
3015
|
+
semaphore,
|
|
3016
|
+
() => client.getFileContent(owner, repo, file.path, resolvedSha)
|
|
3017
|
+
);
|
|
3018
|
+
const byteLength = Buffer.byteLength(content, "utf8");
|
|
3019
|
+
if (byteLength > MAX_FILE_SIZE) {
|
|
3020
|
+
logger5.warn(
|
|
3021
|
+
`Skipping "${file.path}" from ${sourceKey}: fetched ${(byteLength / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit.`
|
|
3022
|
+
);
|
|
3023
|
+
continue;
|
|
3024
|
+
}
|
|
3025
|
+
if (basename(file.path) === SKILL_FILE_NAME2) {
|
|
3026
|
+
try {
|
|
3027
|
+
content = injectSourceMetadata({
|
|
3028
|
+
content,
|
|
3029
|
+
source: sourceUrl,
|
|
3030
|
+
repository,
|
|
3031
|
+
ref: provenanceRef
|
|
3032
|
+
});
|
|
3033
|
+
} catch {
|
|
3034
|
+
logger5.warn(
|
|
3035
|
+
`Frontmatter in ${file.path} (${sourceKey}) is invalid. Prepending a fresh provenance block.`
|
|
3036
|
+
);
|
|
3037
|
+
content = `---
|
|
3038
|
+
source: ${sourceUrl}
|
|
3039
|
+
repository: ${repository}
|
|
3040
|
+
ref: ${provenanceRef}
|
|
3041
|
+
---
|
|
3042
|
+
${content}`;
|
|
3043
|
+
}
|
|
3044
|
+
}
|
|
3045
|
+
const absolutePath = join9(scopeRoot, deployRelative);
|
|
3046
|
+
deployed.push({ relativeToScopeRoot: deployRelative, absolutePath, content });
|
|
3047
|
+
if (!frozen) {
|
|
3048
|
+
await writeFileContent(absolutePath, content);
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
deployed.sort(
|
|
3052
|
+
(a, b) => a.relativeToScopeRoot < b.relativeToScopeRoot ? -1 : a.relativeToScopeRoot > b.relativeToScopeRoot ? 1 : 0
|
|
3053
|
+
);
|
|
3054
|
+
const deployedFiles = deployed.map((d) => d.relativeToScopeRoot);
|
|
3055
|
+
const contentHash = computeContentHash2(deployed);
|
|
3056
|
+
if (frozen && locked?.content_hash) {
|
|
3057
|
+
if (RULESYNC_CONTENT_HASH_REGEX2.test(locked.content_hash)) {
|
|
3058
|
+
if (locked.content_hash !== contentHash) {
|
|
3059
|
+
throw new Error(
|
|
3060
|
+
`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.`
|
|
3061
|
+
);
|
|
3062
|
+
}
|
|
3063
|
+
} else {
|
|
3064
|
+
logger5.debug(
|
|
3065
|
+
`Skipping content_hash integrity check for ${sourceKey} skill "${sk.name}": recorded hash "${locked.content_hash}" was not written by rulesync.`
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
const installation = {
|
|
3070
|
+
source: sourceKey,
|
|
3071
|
+
owner,
|
|
3072
|
+
repo,
|
|
3073
|
+
agent,
|
|
3074
|
+
scope,
|
|
3075
|
+
skill: sk.name,
|
|
3076
|
+
resolved_ref: resolvedRef,
|
|
3077
|
+
resolved_commit: resolvedSha,
|
|
3078
|
+
install_dir: toPosixPath(installRelDir),
|
|
3079
|
+
deployed_files: deployedFiles,
|
|
3080
|
+
content_hash: contentHash
|
|
3081
|
+
};
|
|
3082
|
+
if (rs.ref !== void 0) {
|
|
3083
|
+
installation.requested_ref = rs.ref;
|
|
3084
|
+
}
|
|
3085
|
+
results.push({ installation, deployed });
|
|
3086
|
+
logger5.info(
|
|
3087
|
+
`Installed gh skill "${sk.name}" from ${sourceKey} (agent=${agent}, scope=${scope}, ref=${resolvedRef})`
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
return results;
|
|
3091
|
+
}
|
|
3092
|
+
async function removeStaleFile(params) {
|
|
3093
|
+
const { relativePath, scope, baseDir, logger: logger5 } = params;
|
|
3094
|
+
if (posix3.isAbsolute(relativePath) || relativePath.split(/[/\\]/).includes("..")) {
|
|
3095
|
+
logger5.warn(`Refusing to remove stale gh file with suspicious path: "${relativePath}".`);
|
|
3096
|
+
return;
|
|
3097
|
+
}
|
|
3098
|
+
const scopeRoot = scope === "user" ? getHomeDirectory() : baseDir;
|
|
3099
|
+
try {
|
|
3100
|
+
checkPathTraversal({ relativePath, intendedRootDir: scopeRoot });
|
|
3101
|
+
} catch {
|
|
3102
|
+
logger5.warn(`Refusing to remove stale gh file outside ${scope} root: "${relativePath}".`);
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
const absolute = join9(scopeRoot, relativePath);
|
|
3106
|
+
await removeFile(absolute);
|
|
3107
|
+
logger5.debug(`Removed stale gh file: ${relativePath}`);
|
|
3108
|
+
}
|
|
3109
|
+
function is404(error) {
|
|
3110
|
+
if (error instanceof GitHubClientError && error.statusCode === 404) {
|
|
3111
|
+
return true;
|
|
3112
|
+
}
|
|
3113
|
+
if (typeof error === "object" && error !== null && "statusCode" in error && error.statusCode === 404) {
|
|
3114
|
+
return true;
|
|
3115
|
+
}
|
|
3116
|
+
return false;
|
|
3117
|
+
}
|
|
3118
|
+
function computeContentHash2(files) {
|
|
3119
|
+
const hash = createHash2("sha256");
|
|
3120
|
+
for (const { relativeToScopeRoot, content } of files) {
|
|
3121
|
+
hash.update(relativeToScopeRoot);
|
|
3122
|
+
hash.update("\0");
|
|
3123
|
+
hash.update(content);
|
|
3124
|
+
hash.update("\0");
|
|
3125
|
+
}
|
|
3126
|
+
return `sha256:${hash.digest("hex")}`;
|
|
3127
|
+
}
|
|
3128
|
+
|
|
3129
|
+
// src/lib/sources.ts
|
|
3130
|
+
import { join as join12, resolve, sep } from "path";
|
|
3131
|
+
import { Semaphore as Semaphore4 } from "es-toolkit/promise";
|
|
3132
|
+
|
|
1955
3133
|
// src/lib/git-client.ts
|
|
1956
3134
|
import { execFile } from "child_process";
|
|
1957
|
-
import { isAbsolute, join as
|
|
3135
|
+
import { isAbsolute, join as join10, relative } from "path";
|
|
1958
3136
|
import { promisify } from "util";
|
|
1959
3137
|
var execFileAsync = promisify(execFile);
|
|
1960
3138
|
var GIT_TIMEOUT_MS = 6e4;
|
|
@@ -2076,7 +3254,7 @@ async function fetchSkillFiles(params) {
|
|
|
2076
3254
|
timeout: GIT_TIMEOUT_MS
|
|
2077
3255
|
});
|
|
2078
3256
|
await execFileAsync("git", ["-C", tmpDir, "checkout"], { timeout: GIT_TIMEOUT_MS });
|
|
2079
|
-
const skillsDir =
|
|
3257
|
+
const skillsDir = join10(tmpDir, skillsPath);
|
|
2080
3258
|
if (!await directoryExists(skillsDir)) return [];
|
|
2081
3259
|
return await walkDirectory(skillsDir, skillsDir, 0, { totalFiles: 0, totalSize: 0 }, logger5);
|
|
2082
3260
|
} catch (error) {
|
|
@@ -2098,7 +3276,7 @@ async function walkDirectory(dir, baseDir, depth = 0, ctx = { totalFiles: 0, tot
|
|
|
2098
3276
|
const results = [];
|
|
2099
3277
|
for (const name of await listDirectoryFiles(dir)) {
|
|
2100
3278
|
if (name === ".git") continue;
|
|
2101
|
-
const fullPath =
|
|
3279
|
+
const fullPath = join10(dir, name);
|
|
2102
3280
|
if (await isSymlink(fullPath)) {
|
|
2103
3281
|
logger5?.warn(`Skipping symlink "${fullPath}".`);
|
|
2104
3282
|
continue;
|
|
@@ -2133,29 +3311,29 @@ async function walkDirectory(dir, baseDir, depth = 0, ctx = { totalFiles: 0, tot
|
|
|
2133
3311
|
}
|
|
2134
3312
|
|
|
2135
3313
|
// src/lib/sources-lock.ts
|
|
2136
|
-
import { createHash } from "crypto";
|
|
2137
|
-
import { join as
|
|
2138
|
-
import { optional, refine, z as
|
|
3314
|
+
import { createHash as createHash3 } from "crypto";
|
|
3315
|
+
import { join as join11 } from "path";
|
|
3316
|
+
import { optional as optional4, refine as refine3, z as z7 } from "zod/mini";
|
|
2139
3317
|
var LOCKFILE_VERSION = 1;
|
|
2140
|
-
var LockedSkillSchema =
|
|
2141
|
-
integrity:
|
|
3318
|
+
var LockedSkillSchema = z7.object({
|
|
3319
|
+
integrity: z7.string()
|
|
2142
3320
|
});
|
|
2143
|
-
var LockedSourceSchema =
|
|
2144
|
-
requestedRef:
|
|
2145
|
-
resolvedRef:
|
|
2146
|
-
resolvedAt:
|
|
2147
|
-
skills:
|
|
3321
|
+
var LockedSourceSchema = z7.object({
|
|
3322
|
+
requestedRef: optional4(z7.string()),
|
|
3323
|
+
resolvedRef: z7.string().check(refine3((v) => /^[0-9a-f]{40}$/.test(v), "resolvedRef must be a 40-character hex SHA")),
|
|
3324
|
+
resolvedAt: optional4(z7.string()),
|
|
3325
|
+
skills: z7.record(z7.string(), LockedSkillSchema)
|
|
2148
3326
|
});
|
|
2149
|
-
var SourcesLockSchema =
|
|
2150
|
-
lockfileVersion:
|
|
2151
|
-
sources:
|
|
3327
|
+
var SourcesLockSchema = z7.object({
|
|
3328
|
+
lockfileVersion: z7.number(),
|
|
3329
|
+
sources: z7.record(z7.string(), LockedSourceSchema)
|
|
2152
3330
|
});
|
|
2153
|
-
var LegacyLockedSourceSchema =
|
|
2154
|
-
resolvedRef:
|
|
2155
|
-
skills:
|
|
3331
|
+
var LegacyLockedSourceSchema = z7.object({
|
|
3332
|
+
resolvedRef: z7.string(),
|
|
3333
|
+
skills: z7.array(z7.string())
|
|
2156
3334
|
});
|
|
2157
|
-
var LegacySourcesLockSchema =
|
|
2158
|
-
sources:
|
|
3335
|
+
var LegacySourcesLockSchema = z7.object({
|
|
3336
|
+
sources: z7.record(z7.string(), LegacyLockedSourceSchema)
|
|
2159
3337
|
});
|
|
2160
3338
|
function migrateLegacyLock(params) {
|
|
2161
3339
|
const { legacy, logger: logger5 } = params;
|
|
@@ -2180,7 +3358,7 @@ function createEmptyLock() {
|
|
|
2180
3358
|
}
|
|
2181
3359
|
async function readLockFile(params) {
|
|
2182
3360
|
const { logger: logger5 } = params;
|
|
2183
|
-
const lockPath =
|
|
3361
|
+
const lockPath = join11(params.baseDir, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH);
|
|
2184
3362
|
if (!await fileExists(lockPath)) {
|
|
2185
3363
|
logger5.debug("No sources lockfile found, starting fresh.");
|
|
2186
3364
|
return createEmptyLock();
|
|
@@ -2209,13 +3387,13 @@ async function readLockFile(params) {
|
|
|
2209
3387
|
}
|
|
2210
3388
|
async function writeLockFile(params) {
|
|
2211
3389
|
const { logger: logger5 } = params;
|
|
2212
|
-
const lockPath =
|
|
3390
|
+
const lockPath = join11(params.baseDir, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH);
|
|
2213
3391
|
const content = JSON.stringify(params.lock, null, 2) + "\n";
|
|
2214
3392
|
await writeFileContent(lockPath, content);
|
|
2215
3393
|
logger5.debug(`Wrote sources lockfile to ${lockPath}`);
|
|
2216
3394
|
}
|
|
2217
3395
|
function computeSkillIntegrity(files) {
|
|
2218
|
-
const hash =
|
|
3396
|
+
const hash = createHash3("sha256");
|
|
2219
3397
|
const sorted = files.toSorted((a, b) => a.path.localeCompare(b.path));
|
|
2220
3398
|
for (const file of sorted) {
|
|
2221
3399
|
hash.update(file.path);
|
|
@@ -2383,7 +3561,7 @@ function logGitClientHints(params) {
|
|
|
2383
3561
|
async function checkLockedSkillsExist(curatedDir, skillNames) {
|
|
2384
3562
|
if (skillNames.length === 0) return true;
|
|
2385
3563
|
for (const name of skillNames) {
|
|
2386
|
-
if (!await directoryExists(
|
|
3564
|
+
if (!await directoryExists(join12(curatedDir, name))) {
|
|
2387
3565
|
return false;
|
|
2388
3566
|
}
|
|
2389
3567
|
}
|
|
@@ -2393,7 +3571,7 @@ async function cleanPreviousCuratedSkills(params) {
|
|
|
2393
3571
|
const { curatedDir, lockedSkillNames, logger: logger5 } = params;
|
|
2394
3572
|
const resolvedCuratedDir = resolve(curatedDir);
|
|
2395
3573
|
for (const prevSkill of lockedSkillNames) {
|
|
2396
|
-
const prevDir =
|
|
3574
|
+
const prevDir = join12(curatedDir, prevSkill);
|
|
2397
3575
|
if (!resolve(prevDir).startsWith(resolvedCuratedDir + sep)) {
|
|
2398
3576
|
logger5.warn(
|
|
2399
3577
|
`Skipping removal of "${prevSkill}": resolved path is outside the curated directory.`
|
|
@@ -2433,9 +3611,9 @@ async function writeSkillAndComputeIntegrity(params) {
|
|
|
2433
3611
|
for (const file of files) {
|
|
2434
3612
|
checkPathTraversal({
|
|
2435
3613
|
relativePath: file.relativePath,
|
|
2436
|
-
intendedRootDir:
|
|
3614
|
+
intendedRootDir: join12(curatedDir, skillName)
|
|
2437
3615
|
});
|
|
2438
|
-
await writeFileContent(
|
|
3616
|
+
await writeFileContent(join12(curatedDir, skillName, file.relativePath), file.content);
|
|
2439
3617
|
written.push({ path: file.relativePath, content: file.content });
|
|
2440
3618
|
}
|
|
2441
3619
|
const integrity = computeSkillIntegrity(written);
|
|
@@ -2479,6 +3657,40 @@ function buildLockUpdate(params) {
|
|
|
2479
3657
|
);
|
|
2480
3658
|
return { updatedLock, fetchedNames };
|
|
2481
3659
|
}
|
|
3660
|
+
function getFirstPathSeparatorIndex(path2) {
|
|
3661
|
+
const slashIndex = path2.indexOf("/");
|
|
3662
|
+
const backslashIndex = path2.indexOf("\\");
|
|
3663
|
+
if (slashIndex === -1) return backslashIndex;
|
|
3664
|
+
if (backslashIndex === -1) return slashIndex;
|
|
3665
|
+
return Math.min(slashIndex, backslashIndex);
|
|
3666
|
+
}
|
|
3667
|
+
function groupRemoteFilesBySkillRoot(params) {
|
|
3668
|
+
const { remoteFiles, skillFilter, isWildcard } = params;
|
|
3669
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3670
|
+
const rootLevelFiles = [];
|
|
3671
|
+
for (const file of remoteFiles) {
|
|
3672
|
+
const separatorIndex = getFirstPathSeparatorIndex(file.relativePath);
|
|
3673
|
+
if (separatorIndex === -1) {
|
|
3674
|
+
rootLevelFiles.push(file);
|
|
3675
|
+
continue;
|
|
3676
|
+
}
|
|
3677
|
+
const skillName = file.relativePath.substring(0, separatorIndex);
|
|
3678
|
+
if (skillName.length === 0) {
|
|
3679
|
+
continue;
|
|
3680
|
+
}
|
|
3681
|
+
const innerPath = file.relativePath.substring(separatorIndex + 1);
|
|
3682
|
+
const groupedFiles = grouped.get(skillName) ?? [];
|
|
3683
|
+
groupedFiles.push({ relativePath: innerPath, content: file.content });
|
|
3684
|
+
grouped.set(skillName, groupedFiles);
|
|
3685
|
+
}
|
|
3686
|
+
if (grouped.size === 0 && !isWildcard && skillFilter.length === 1) {
|
|
3687
|
+
const [singleSkillName] = skillFilter;
|
|
3688
|
+
if (singleSkillName !== void 0 && rootLevelFiles.length > 0) {
|
|
3689
|
+
grouped.set(singleSkillName, rootLevelFiles);
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
return grouped;
|
|
3693
|
+
}
|
|
2482
3694
|
async function fetchSource(params) {
|
|
2483
3695
|
const {
|
|
2484
3696
|
sourceEntry,
|
|
@@ -2512,7 +3724,7 @@ async function fetchSource(params) {
|
|
|
2512
3724
|
ref = resolvedSha;
|
|
2513
3725
|
logger5.debug(`Resolved ${sourceKey} ref "${requestedRef}" to SHA: ${resolvedSha}`);
|
|
2514
3726
|
}
|
|
2515
|
-
const curatedDir =
|
|
3727
|
+
const curatedDir = join12(baseDir, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
|
|
2516
3728
|
if (locked && resolvedSha === locked.resolvedRef && !updateSources) {
|
|
2517
3729
|
const allExist = await checkLockedSkillsExist(curatedDir, lockedSkillNames);
|
|
2518
3730
|
if (allExist) {
|
|
@@ -2526,11 +3738,60 @@ async function fetchSource(params) {
|
|
|
2526
3738
|
}
|
|
2527
3739
|
const skillFilter = sourceEntry.skills ?? ["*"];
|
|
2528
3740
|
const isWildcard = skillFilter.length === 1 && skillFilter[0] === "*";
|
|
3741
|
+
const semaphore = new Semaphore4(FETCH_CONCURRENCY_LIMIT);
|
|
3742
|
+
const fetchedSkills = {};
|
|
2529
3743
|
const skillsBasePath = parsed.path ?? "skills";
|
|
2530
3744
|
let remoteSkillDirs;
|
|
3745
|
+
let remoteSkillNames = [];
|
|
3746
|
+
let fallbackHandled = false;
|
|
2531
3747
|
try {
|
|
2532
3748
|
const entries = await client.listDirectory(parsed.owner, parsed.repo, skillsBasePath, ref);
|
|
2533
3749
|
remoteSkillDirs = entries.filter((e) => e.type === "dir").map((e) => ({ name: e.name, path: e.path }));
|
|
3750
|
+
if (remoteSkillDirs.length === 0 && !isWildcard && skillFilter.length === 1) {
|
|
3751
|
+
const rootFiles = entries.filter((entry) => entry.type === "file");
|
|
3752
|
+
const rootSkillFiles = [];
|
|
3753
|
+
for (const file of rootFiles) {
|
|
3754
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
3755
|
+
logger5.warn(
|
|
3756
|
+
`Skipping file "${file.path}" (${(file.size / 1024 / 1024).toFixed(2)}MB exceeds ${MAX_FILE_SIZE / 1024 / 1024}MB limit).`
|
|
3757
|
+
);
|
|
3758
|
+
continue;
|
|
3759
|
+
}
|
|
3760
|
+
const content = await withSemaphore(
|
|
3761
|
+
semaphore,
|
|
3762
|
+
() => client.getFileContent(parsed.owner, parsed.repo, file.path, ref)
|
|
3763
|
+
);
|
|
3764
|
+
rootSkillFiles.push({ relativePath: file.name, content });
|
|
3765
|
+
}
|
|
3766
|
+
const groupedRootFiles = groupRemoteFilesBySkillRoot({
|
|
3767
|
+
remoteFiles: rootSkillFiles,
|
|
3768
|
+
skillFilter,
|
|
3769
|
+
isWildcard
|
|
3770
|
+
});
|
|
3771
|
+
const [fallbackSkillName] = groupedRootFiles.keys();
|
|
3772
|
+
if (fallbackSkillName !== void 0) {
|
|
3773
|
+
fallbackHandled = true;
|
|
3774
|
+
remoteSkillNames = [fallbackSkillName];
|
|
3775
|
+
if (!shouldSkipSkill({
|
|
3776
|
+
skillName: fallbackSkillName,
|
|
3777
|
+
sourceKey,
|
|
3778
|
+
localSkillNames,
|
|
3779
|
+
alreadyFetchedSkillNames,
|
|
3780
|
+
logger: logger5
|
|
3781
|
+
})) {
|
|
3782
|
+
fetchedSkills[fallbackSkillName] = await writeSkillAndComputeIntegrity({
|
|
3783
|
+
skillName: fallbackSkillName,
|
|
3784
|
+
files: groupedRootFiles.get(fallbackSkillName) ?? [],
|
|
3785
|
+
curatedDir,
|
|
3786
|
+
locked,
|
|
3787
|
+
resolvedSha,
|
|
3788
|
+
sourceKey,
|
|
3789
|
+
logger: logger5
|
|
3790
|
+
});
|
|
3791
|
+
logger5.debug(`Fetched skill "${fallbackSkillName}" from ${sourceKey}`);
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
2534
3795
|
} catch (error) {
|
|
2535
3796
|
if (error instanceof GitHubClientError && error.statusCode === 404) {
|
|
2536
3797
|
logger5.warn(`No skills/ directory found in ${sourceKey}. Skipping.`);
|
|
@@ -2539,8 +3800,9 @@ async function fetchSource(params) {
|
|
|
2539
3800
|
throw error;
|
|
2540
3801
|
}
|
|
2541
3802
|
const filteredDirs = isWildcard ? remoteSkillDirs : remoteSkillDirs.filter((d) => skillFilter.includes(d.name));
|
|
2542
|
-
|
|
2543
|
-
|
|
3803
|
+
if (!fallbackHandled) {
|
|
3804
|
+
remoteSkillNames = filteredDirs.map((d) => d.name);
|
|
3805
|
+
}
|
|
2544
3806
|
if (locked) {
|
|
2545
3807
|
await cleanPreviousCuratedSkills({ curatedDir, lockedSkillNames, logger: logger5 });
|
|
2546
3808
|
}
|
|
@@ -2598,7 +3860,7 @@ async function fetchSource(params) {
|
|
|
2598
3860
|
locked,
|
|
2599
3861
|
requestedRef,
|
|
2600
3862
|
resolvedSha,
|
|
2601
|
-
remoteSkillNames
|
|
3863
|
+
remoteSkillNames,
|
|
2602
3864
|
logger: logger5
|
|
2603
3865
|
});
|
|
2604
3866
|
return {
|
|
@@ -2637,7 +3899,7 @@ async function fetchSourceViaGit(params) {
|
|
|
2637
3899
|
requestedRef = def.ref;
|
|
2638
3900
|
resolvedSha = def.sha;
|
|
2639
3901
|
}
|
|
2640
|
-
const curatedDir =
|
|
3902
|
+
const curatedDir = join12(baseDir, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
|
|
2641
3903
|
if (locked && resolvedSha === locked.resolvedRef && !updateSources) {
|
|
2642
3904
|
if (await checkLockedSkillsExist(curatedDir, lockedSkillNames)) {
|
|
2643
3905
|
return { skillCount: 0, fetchedSkillNames: lockedSkillNames, updatedLock: lock };
|
|
@@ -2660,28 +3922,7 @@ async function fetchSourceViaGit(params) {
|
|
|
2660
3922
|
ref: requestedRef,
|
|
2661
3923
|
skillsPath: sourceEntry.path ?? "skills"
|
|
2662
3924
|
});
|
|
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
|
-
}
|
|
3925
|
+
const skillFileMap = groupRemoteFilesBySkillRoot({ remoteFiles, skillFilter, isWildcard });
|
|
2685
3926
|
const allNames = [...skillFileMap.keys()];
|
|
2686
3927
|
const filteredNames = isWildcard ? allNames : allNames.filter((n) => skillFilter.includes(n));
|
|
2687
3928
|
if (locked) {
|
|
@@ -2726,21 +3967,47 @@ async function fetchSourceViaGit(params) {
|
|
|
2726
3967
|
}
|
|
2727
3968
|
|
|
2728
3969
|
// src/cli/commands/install.ts
|
|
3970
|
+
var INSTALL_MODES = ["rulesync", "apm", "gh"];
|
|
2729
3971
|
async function installCommand(logger5, options) {
|
|
3972
|
+
const mode = options.mode ?? "rulesync";
|
|
3973
|
+
if (mode === "gh") {
|
|
3974
|
+
await runGhInstall(logger5, options);
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3977
|
+
if (mode === "apm") {
|
|
3978
|
+
await runApmInstall(logger5, options);
|
|
3979
|
+
return;
|
|
3980
|
+
}
|
|
3981
|
+
await runRulesyncInstall(logger5, options);
|
|
3982
|
+
}
|
|
3983
|
+
async function runRulesyncInstall(logger5, options) {
|
|
3984
|
+
const baseDir = process.cwd();
|
|
3985
|
+
const apmExists = await apmManifestExists(baseDir);
|
|
2730
3986
|
const config = await ConfigResolver.resolve({
|
|
2731
3987
|
configPath: options.configPath,
|
|
2732
3988
|
verbose: options.verbose,
|
|
2733
3989
|
silent: options.silent
|
|
2734
3990
|
});
|
|
2735
3991
|
const sources = config.getSources();
|
|
3992
|
+
if (apmExists && sources.length > 0) {
|
|
3993
|
+
throw new Error(
|
|
3994
|
+
"Both apm.yml and rulesync.jsonc `sources` are defined. Pass --mode apm or --mode rulesync to disambiguate."
|
|
3995
|
+
);
|
|
3996
|
+
}
|
|
2736
3997
|
if (sources.length === 0) {
|
|
3998
|
+
if (apmExists) {
|
|
3999
|
+
logger5.warn(
|
|
4000
|
+
"No sources defined in rulesync.jsonc, but apm.yml is present. Did you mean --mode apm?"
|
|
4001
|
+
);
|
|
4002
|
+
return;
|
|
4003
|
+
}
|
|
2737
4004
|
logger5.warn("No sources defined in configuration. Nothing to install.");
|
|
2738
4005
|
return;
|
|
2739
4006
|
}
|
|
2740
4007
|
logger5.debug(`Installing skills from ${sources.length} source(s)...`);
|
|
2741
4008
|
const result = await resolveAndFetchSources({
|
|
2742
4009
|
sources,
|
|
2743
|
-
baseDir
|
|
4010
|
+
baseDir,
|
|
2744
4011
|
options: {
|
|
2745
4012
|
updateSources: options.update,
|
|
2746
4013
|
frozen: options.frozen,
|
|
@@ -2760,21 +4027,95 @@ async function installCommand(logger5, options) {
|
|
|
2760
4027
|
logger5.success(`All skills up to date (${result.sourcesProcessed} source(s) checked).`);
|
|
2761
4028
|
}
|
|
2762
4029
|
}
|
|
4030
|
+
async function runApmInstall(logger5, options) {
|
|
4031
|
+
const baseDir = process.cwd();
|
|
4032
|
+
if (!await apmManifestExists(baseDir)) {
|
|
4033
|
+
throw new Error(
|
|
4034
|
+
"--mode apm requires an apm.yml at the project root. Create one or drop --mode apm to fall back to rulesync mode."
|
|
4035
|
+
);
|
|
4036
|
+
}
|
|
4037
|
+
const result = await installApm({
|
|
4038
|
+
baseDir,
|
|
4039
|
+
options: {
|
|
4040
|
+
update: options.update,
|
|
4041
|
+
frozen: options.frozen,
|
|
4042
|
+
token: options.token
|
|
4043
|
+
},
|
|
4044
|
+
logger: logger5
|
|
4045
|
+
});
|
|
4046
|
+
if (logger5.jsonMode) {
|
|
4047
|
+
logger5.captureData("dependenciesProcessed", result.dependenciesProcessed);
|
|
4048
|
+
logger5.captureData("deployedFileCount", result.deployedFileCount);
|
|
4049
|
+
logger5.captureData("failedDependencyCount", result.failedDependencyCount);
|
|
4050
|
+
}
|
|
4051
|
+
if (result.failedDependencyCount > 0) {
|
|
4052
|
+
throw new Error(
|
|
4053
|
+
`Failed to install ${result.failedDependencyCount} of ${result.dependenciesProcessed} apm dependency(ies). See the log above for details.`
|
|
4054
|
+
);
|
|
4055
|
+
}
|
|
4056
|
+
if (result.deployedFileCount > 0) {
|
|
4057
|
+
logger5.success(
|
|
4058
|
+
`Installed ${result.deployedFileCount} file(s) from ${result.dependenciesProcessed} apm dependency(ies).`
|
|
4059
|
+
);
|
|
4060
|
+
} else {
|
|
4061
|
+
logger5.success(`All apm dependencies up to date (${result.dependenciesProcessed} checked).`);
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
async function runGhInstall(logger5, options) {
|
|
4065
|
+
const baseDir = process.cwd();
|
|
4066
|
+
const config = await ConfigResolver.resolve({
|
|
4067
|
+
configPath: options.configPath,
|
|
4068
|
+
verbose: options.verbose,
|
|
4069
|
+
silent: options.silent
|
|
4070
|
+
});
|
|
4071
|
+
const sources = config.getSources();
|
|
4072
|
+
if (sources.length === 0) {
|
|
4073
|
+
logger5.warn("No sources defined in configuration. Nothing to install.");
|
|
4074
|
+
return;
|
|
4075
|
+
}
|
|
4076
|
+
const result = await installGh({
|
|
4077
|
+
baseDir,
|
|
4078
|
+
sources,
|
|
4079
|
+
options: {
|
|
4080
|
+
update: options.update,
|
|
4081
|
+
frozen: options.frozen,
|
|
4082
|
+
token: options.token
|
|
4083
|
+
},
|
|
4084
|
+
logger: logger5
|
|
4085
|
+
});
|
|
4086
|
+
if (logger5.jsonMode) {
|
|
4087
|
+
logger5.captureData("sourcesProcessed", result.sourcesProcessed);
|
|
4088
|
+
logger5.captureData("installedSkillCount", result.installedSkillCount);
|
|
4089
|
+
logger5.captureData("failedSourceCount", result.failedSourceCount);
|
|
4090
|
+
}
|
|
4091
|
+
if (result.failedSourceCount > 0) {
|
|
4092
|
+
throw new Error(
|
|
4093
|
+
`Failed to install ${result.failedSourceCount} of ${result.sourcesProcessed} gh source(s). See the log above for details.`
|
|
4094
|
+
);
|
|
4095
|
+
}
|
|
4096
|
+
if (result.installedSkillCount > 0) {
|
|
4097
|
+
logger5.success(
|
|
4098
|
+
`Installed ${result.installedSkillCount} skill(s) from ${result.sourcesProcessed} gh source(s).`
|
|
4099
|
+
);
|
|
4100
|
+
} else {
|
|
4101
|
+
logger5.success(`All gh sources up to date (${result.sourcesProcessed} checked).`);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
2763
4104
|
|
|
2764
4105
|
// src/cli/commands/mcp.ts
|
|
2765
4106
|
import { FastMCP } from "fastmcp";
|
|
2766
4107
|
|
|
2767
4108
|
// src/mcp/tools.ts
|
|
2768
|
-
import { z as
|
|
4109
|
+
import { z as z18 } from "zod/mini";
|
|
2769
4110
|
|
|
2770
4111
|
// src/mcp/commands.ts
|
|
2771
|
-
import { basename, join as
|
|
2772
|
-
import { z as
|
|
4112
|
+
import { basename as basename2, join as join13 } from "path";
|
|
4113
|
+
import { z as z8 } from "zod/mini";
|
|
2773
4114
|
var logger = new ConsoleLogger({ verbose: false, silent: true });
|
|
2774
4115
|
var maxCommandSizeBytes = 1024 * 1024;
|
|
2775
4116
|
var maxCommandsCount = 1e3;
|
|
2776
4117
|
async function listCommands() {
|
|
2777
|
-
const commandsDir =
|
|
4118
|
+
const commandsDir = join13(process.cwd(), RULESYNC_COMMANDS_RELATIVE_DIR_PATH);
|
|
2778
4119
|
try {
|
|
2779
4120
|
const files = await listDirectoryFiles(commandsDir);
|
|
2780
4121
|
const mdFiles = files.filter((file) => file.endsWith(".md"));
|
|
@@ -2790,7 +4131,7 @@ async function listCommands() {
|
|
|
2790
4131
|
});
|
|
2791
4132
|
const frontmatter = command.getFrontmatter();
|
|
2792
4133
|
return {
|
|
2793
|
-
relativePathFromCwd:
|
|
4134
|
+
relativePathFromCwd: join13(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, file),
|
|
2794
4135
|
frontmatter
|
|
2795
4136
|
};
|
|
2796
4137
|
} catch (error) {
|
|
@@ -2812,13 +4153,13 @@ async function getCommand({ relativePathFromCwd }) {
|
|
|
2812
4153
|
relativePath: relativePathFromCwd,
|
|
2813
4154
|
intendedRootDir: process.cwd()
|
|
2814
4155
|
});
|
|
2815
|
-
const filename =
|
|
4156
|
+
const filename = basename2(relativePathFromCwd);
|
|
2816
4157
|
try {
|
|
2817
4158
|
const command = await RulesyncCommand.fromFile({
|
|
2818
4159
|
relativeFilePath: filename
|
|
2819
4160
|
});
|
|
2820
4161
|
return {
|
|
2821
|
-
relativePathFromCwd:
|
|
4162
|
+
relativePathFromCwd: join13(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename),
|
|
2822
4163
|
frontmatter: command.getFrontmatter(),
|
|
2823
4164
|
body: command.getBody()
|
|
2824
4165
|
};
|
|
@@ -2837,7 +4178,7 @@ async function putCommand({
|
|
|
2837
4178
|
relativePath: relativePathFromCwd,
|
|
2838
4179
|
intendedRootDir: process.cwd()
|
|
2839
4180
|
});
|
|
2840
|
-
const filename =
|
|
4181
|
+
const filename = basename2(relativePathFromCwd);
|
|
2841
4182
|
const estimatedSize = JSON.stringify(frontmatter).length + body.length;
|
|
2842
4183
|
if (estimatedSize > maxCommandSizeBytes) {
|
|
2843
4184
|
throw new Error(
|
|
@@ -2847,7 +4188,7 @@ async function putCommand({
|
|
|
2847
4188
|
try {
|
|
2848
4189
|
const existingCommands = await listCommands();
|
|
2849
4190
|
const isUpdate = existingCommands.some(
|
|
2850
|
-
(command2) => command2.relativePathFromCwd ===
|
|
4191
|
+
(command2) => command2.relativePathFromCwd === join13(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename)
|
|
2851
4192
|
);
|
|
2852
4193
|
if (!isUpdate && existingCommands.length >= maxCommandsCount) {
|
|
2853
4194
|
throw new Error(
|
|
@@ -2864,11 +4205,11 @@ async function putCommand({
|
|
|
2864
4205
|
fileContent,
|
|
2865
4206
|
validate: true
|
|
2866
4207
|
});
|
|
2867
|
-
const commandsDir =
|
|
4208
|
+
const commandsDir = join13(process.cwd(), RULESYNC_COMMANDS_RELATIVE_DIR_PATH);
|
|
2868
4209
|
await ensureDir(commandsDir);
|
|
2869
4210
|
await writeFileContent(command.getFilePath(), command.getFileContent());
|
|
2870
4211
|
return {
|
|
2871
|
-
relativePathFromCwd:
|
|
4212
|
+
relativePathFromCwd: join13(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename),
|
|
2872
4213
|
frontmatter: command.getFrontmatter(),
|
|
2873
4214
|
body: command.getBody()
|
|
2874
4215
|
};
|
|
@@ -2883,12 +4224,12 @@ async function deleteCommand({ relativePathFromCwd }) {
|
|
|
2883
4224
|
relativePath: relativePathFromCwd,
|
|
2884
4225
|
intendedRootDir: process.cwd()
|
|
2885
4226
|
});
|
|
2886
|
-
const filename =
|
|
2887
|
-
const fullPath =
|
|
4227
|
+
const filename = basename2(relativePathFromCwd);
|
|
4228
|
+
const fullPath = join13(process.cwd(), RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename);
|
|
2888
4229
|
try {
|
|
2889
4230
|
await removeFile(fullPath);
|
|
2890
4231
|
return {
|
|
2891
|
-
relativePathFromCwd:
|
|
4232
|
+
relativePathFromCwd: join13(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename)
|
|
2892
4233
|
};
|
|
2893
4234
|
} catch (error) {
|
|
2894
4235
|
throw new Error(`Failed to delete command file ${relativePathFromCwd}: ${formatError(error)}`, {
|
|
@@ -2897,23 +4238,23 @@ async function deleteCommand({ relativePathFromCwd }) {
|
|
|
2897
4238
|
}
|
|
2898
4239
|
}
|
|
2899
4240
|
var commandToolSchemas = {
|
|
2900
|
-
listCommands:
|
|
2901
|
-
getCommand:
|
|
2902
|
-
relativePathFromCwd:
|
|
4241
|
+
listCommands: z8.object({}),
|
|
4242
|
+
getCommand: z8.object({
|
|
4243
|
+
relativePathFromCwd: z8.string()
|
|
2903
4244
|
}),
|
|
2904
|
-
putCommand:
|
|
2905
|
-
relativePathFromCwd:
|
|
4245
|
+
putCommand: z8.object({
|
|
4246
|
+
relativePathFromCwd: z8.string(),
|
|
2906
4247
|
frontmatter: RulesyncCommandFrontmatterSchema,
|
|
2907
|
-
body:
|
|
4248
|
+
body: z8.string()
|
|
2908
4249
|
}),
|
|
2909
|
-
deleteCommand:
|
|
2910
|
-
relativePathFromCwd:
|
|
4250
|
+
deleteCommand: z8.object({
|
|
4251
|
+
relativePathFromCwd: z8.string()
|
|
2911
4252
|
})
|
|
2912
4253
|
};
|
|
2913
4254
|
var commandTools = {
|
|
2914
4255
|
listCommands: {
|
|
2915
4256
|
name: "listCommands",
|
|
2916
|
-
description: `List all commands from ${
|
|
4257
|
+
description: `List all commands from ${join13(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
|
|
2917
4258
|
parameters: commandToolSchemas.listCommands,
|
|
2918
4259
|
execute: async () => {
|
|
2919
4260
|
const commands = await listCommands();
|
|
@@ -2955,15 +4296,15 @@ var commandTools = {
|
|
|
2955
4296
|
};
|
|
2956
4297
|
|
|
2957
4298
|
// 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:
|
|
4299
|
+
import { z as z9 } from "zod/mini";
|
|
4300
|
+
var generateOptionsSchema = z9.object({
|
|
4301
|
+
targets: z9.optional(z9.array(z9.string())),
|
|
4302
|
+
features: z9.optional(z9.array(z9.string())),
|
|
4303
|
+
delete: z9.optional(z9.boolean()),
|
|
4304
|
+
global: z9.optional(z9.boolean()),
|
|
4305
|
+
simulateCommands: z9.optional(z9.boolean()),
|
|
4306
|
+
simulateSubagents: z9.optional(z9.boolean()),
|
|
4307
|
+
simulateSkills: z9.optional(z9.boolean())
|
|
2967
4308
|
});
|
|
2968
4309
|
async function executeGenerate(options = {}) {
|
|
2969
4310
|
try {
|
|
@@ -3041,12 +4382,139 @@ var generateTools = {
|
|
|
3041
4382
|
}
|
|
3042
4383
|
};
|
|
3043
4384
|
|
|
4385
|
+
// src/mcp/hooks.ts
|
|
4386
|
+
import { join as join14 } from "path";
|
|
4387
|
+
import { z as z10 } from "zod/mini";
|
|
4388
|
+
var maxHooksSizeBytes = 1024 * 1024;
|
|
4389
|
+
async function getHooksFile() {
|
|
4390
|
+
try {
|
|
4391
|
+
const rulesyncHooks = await RulesyncHooks.fromFile({
|
|
4392
|
+
validate: true
|
|
4393
|
+
});
|
|
4394
|
+
const relativePathFromCwd = join14(
|
|
4395
|
+
rulesyncHooks.getRelativeDirPath(),
|
|
4396
|
+
rulesyncHooks.getRelativeFilePath()
|
|
4397
|
+
);
|
|
4398
|
+
return {
|
|
4399
|
+
relativePathFromCwd,
|
|
4400
|
+
content: rulesyncHooks.getFileContent()
|
|
4401
|
+
};
|
|
4402
|
+
} catch (error) {
|
|
4403
|
+
throw new Error(
|
|
4404
|
+
`Failed to read hooks file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4405
|
+
{
|
|
4406
|
+
cause: error
|
|
4407
|
+
}
|
|
4408
|
+
);
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
async function putHooksFile({ content }) {
|
|
4412
|
+
if (content.length > maxHooksSizeBytes) {
|
|
4413
|
+
throw new Error(
|
|
4414
|
+
`Hooks file size ${content.length} bytes exceeds maximum ${maxHooksSizeBytes} bytes (1MB) for ${RULESYNC_HOOKS_RELATIVE_FILE_PATH}`
|
|
4415
|
+
);
|
|
4416
|
+
}
|
|
4417
|
+
try {
|
|
4418
|
+
JSON.parse(content);
|
|
4419
|
+
} catch (error) {
|
|
4420
|
+
throw new Error(
|
|
4421
|
+
`Invalid JSON format in hooks file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4422
|
+
{
|
|
4423
|
+
cause: error
|
|
4424
|
+
}
|
|
4425
|
+
);
|
|
4426
|
+
}
|
|
4427
|
+
try {
|
|
4428
|
+
const baseDir = process.cwd();
|
|
4429
|
+
const paths = RulesyncHooks.getSettablePaths();
|
|
4430
|
+
const relativeDirPath = paths.relativeDirPath;
|
|
4431
|
+
const relativeFilePath = paths.relativeFilePath;
|
|
4432
|
+
const fullPath = join14(baseDir, relativeDirPath, relativeFilePath);
|
|
4433
|
+
const rulesyncHooks = new RulesyncHooks({
|
|
4434
|
+
baseDir,
|
|
4435
|
+
relativeDirPath,
|
|
4436
|
+
relativeFilePath,
|
|
4437
|
+
fileContent: content,
|
|
4438
|
+
validate: true
|
|
4439
|
+
});
|
|
4440
|
+
await ensureDir(join14(baseDir, relativeDirPath));
|
|
4441
|
+
await writeFileContent(fullPath, content);
|
|
4442
|
+
const relativePathFromCwd = join14(relativeDirPath, relativeFilePath);
|
|
4443
|
+
return {
|
|
4444
|
+
relativePathFromCwd,
|
|
4445
|
+
content: rulesyncHooks.getFileContent()
|
|
4446
|
+
};
|
|
4447
|
+
} catch (error) {
|
|
4448
|
+
throw new Error(
|
|
4449
|
+
`Failed to write hooks file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4450
|
+
{
|
|
4451
|
+
cause: error
|
|
4452
|
+
}
|
|
4453
|
+
);
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
async function deleteHooksFile() {
|
|
4457
|
+
try {
|
|
4458
|
+
const baseDir = process.cwd();
|
|
4459
|
+
const paths = RulesyncHooks.getSettablePaths();
|
|
4460
|
+
const filePath = join14(baseDir, paths.relativeDirPath, paths.relativeFilePath);
|
|
4461
|
+
await removeFile(filePath);
|
|
4462
|
+
const relativePathFromCwd = join14(paths.relativeDirPath, paths.relativeFilePath);
|
|
4463
|
+
return {
|
|
4464
|
+
relativePathFromCwd
|
|
4465
|
+
};
|
|
4466
|
+
} catch (error) {
|
|
4467
|
+
throw new Error(
|
|
4468
|
+
`Failed to delete hooks file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4469
|
+
{
|
|
4470
|
+
cause: error
|
|
4471
|
+
}
|
|
4472
|
+
);
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
var hooksToolSchemas = {
|
|
4476
|
+
getHooksFile: z10.object({}),
|
|
4477
|
+
putHooksFile: z10.object({
|
|
4478
|
+
content: z10.string()
|
|
4479
|
+
}),
|
|
4480
|
+
deleteHooksFile: z10.object({})
|
|
4481
|
+
};
|
|
4482
|
+
var hooksTools = {
|
|
4483
|
+
getHooksFile: {
|
|
4484
|
+
name: "getHooksFile",
|
|
4485
|
+
description: `Get the hooks configuration file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}).`,
|
|
4486
|
+
parameters: hooksToolSchemas.getHooksFile,
|
|
4487
|
+
execute: async () => {
|
|
4488
|
+
const result = await getHooksFile();
|
|
4489
|
+
return JSON.stringify(result, null, 2);
|
|
4490
|
+
}
|
|
4491
|
+
},
|
|
4492
|
+
putHooksFile: {
|
|
4493
|
+
name: "putHooksFile",
|
|
4494
|
+
description: "Create or update the hooks configuration file (upsert operation). content parameter is required and must be valid JSON.",
|
|
4495
|
+
parameters: hooksToolSchemas.putHooksFile,
|
|
4496
|
+
execute: async (args) => {
|
|
4497
|
+
const result = await putHooksFile({ content: args.content });
|
|
4498
|
+
return JSON.stringify(result, null, 2);
|
|
4499
|
+
}
|
|
4500
|
+
},
|
|
4501
|
+
deleteHooksFile: {
|
|
4502
|
+
name: "deleteHooksFile",
|
|
4503
|
+
description: "Delete the hooks configuration file.",
|
|
4504
|
+
parameters: hooksToolSchemas.deleteHooksFile,
|
|
4505
|
+
execute: async () => {
|
|
4506
|
+
const result = await deleteHooksFile();
|
|
4507
|
+
return JSON.stringify(result, null, 2);
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
};
|
|
4511
|
+
|
|
3044
4512
|
// src/mcp/ignore.ts
|
|
3045
|
-
import { join as
|
|
3046
|
-
import { z as
|
|
4513
|
+
import { join as join15 } from "path";
|
|
4514
|
+
import { z as z11 } from "zod/mini";
|
|
3047
4515
|
var maxIgnoreFileSizeBytes = 100 * 1024;
|
|
3048
4516
|
async function getIgnoreFile() {
|
|
3049
|
-
const ignoreFilePath =
|
|
4517
|
+
const ignoreFilePath = join15(process.cwd(), RULESYNC_AIIGNORE_RELATIVE_FILE_PATH);
|
|
3050
4518
|
try {
|
|
3051
4519
|
const content = await readFileContent(ignoreFilePath);
|
|
3052
4520
|
return {
|
|
@@ -3063,7 +4531,7 @@ async function getIgnoreFile() {
|
|
|
3063
4531
|
}
|
|
3064
4532
|
}
|
|
3065
4533
|
async function putIgnoreFile({ content }) {
|
|
3066
|
-
const ignoreFilePath =
|
|
4534
|
+
const ignoreFilePath = join15(process.cwd(), RULESYNC_AIIGNORE_RELATIVE_FILE_PATH);
|
|
3067
4535
|
const contentSizeBytes = Buffer.byteLength(content, "utf8");
|
|
3068
4536
|
if (contentSizeBytes > maxIgnoreFileSizeBytes) {
|
|
3069
4537
|
throw new Error(
|
|
@@ -3087,8 +4555,8 @@ async function putIgnoreFile({ content }) {
|
|
|
3087
4555
|
}
|
|
3088
4556
|
}
|
|
3089
4557
|
async function deleteIgnoreFile() {
|
|
3090
|
-
const aiignorePath =
|
|
3091
|
-
const legacyIgnorePath =
|
|
4558
|
+
const aiignorePath = join15(process.cwd(), RULESYNC_AIIGNORE_RELATIVE_FILE_PATH);
|
|
4559
|
+
const legacyIgnorePath = join15(process.cwd(), RULESYNC_IGNORE_RELATIVE_FILE_PATH);
|
|
3092
4560
|
try {
|
|
3093
4561
|
await Promise.all([removeFile(aiignorePath), removeFile(legacyIgnorePath)]);
|
|
3094
4562
|
return {
|
|
@@ -3106,11 +4574,11 @@ async function deleteIgnoreFile() {
|
|
|
3106
4574
|
}
|
|
3107
4575
|
}
|
|
3108
4576
|
var ignoreToolSchemas = {
|
|
3109
|
-
getIgnoreFile:
|
|
3110
|
-
putIgnoreFile:
|
|
3111
|
-
content:
|
|
4577
|
+
getIgnoreFile: z11.object({}),
|
|
4578
|
+
putIgnoreFile: z11.object({
|
|
4579
|
+
content: z11.string()
|
|
3112
4580
|
}),
|
|
3113
|
-
deleteIgnoreFile:
|
|
4581
|
+
deleteIgnoreFile: z11.object({})
|
|
3114
4582
|
};
|
|
3115
4583
|
var ignoreTools = {
|
|
3116
4584
|
getIgnoreFile: {
|
|
@@ -3143,11 +4611,11 @@ var ignoreTools = {
|
|
|
3143
4611
|
};
|
|
3144
4612
|
|
|
3145
4613
|
// src/mcp/import.ts
|
|
3146
|
-
import { z as
|
|
3147
|
-
var importOptionsSchema =
|
|
3148
|
-
target:
|
|
3149
|
-
features:
|
|
3150
|
-
global:
|
|
4614
|
+
import { z as z12 } from "zod/mini";
|
|
4615
|
+
var importOptionsSchema = z12.object({
|
|
4616
|
+
target: z12.string(),
|
|
4617
|
+
features: z12.optional(z12.array(z12.string())),
|
|
4618
|
+
global: z12.optional(z12.boolean())
|
|
3151
4619
|
});
|
|
3152
4620
|
async function executeImport(options) {
|
|
3153
4621
|
try {
|
|
@@ -3218,15 +4686,15 @@ var importTools = {
|
|
|
3218
4686
|
};
|
|
3219
4687
|
|
|
3220
4688
|
// src/mcp/mcp.ts
|
|
3221
|
-
import { join as
|
|
3222
|
-
import { z as
|
|
4689
|
+
import { join as join16 } from "path";
|
|
4690
|
+
import { z as z13 } from "zod/mini";
|
|
3223
4691
|
var maxMcpSizeBytes = 1024 * 1024;
|
|
3224
4692
|
async function getMcpFile() {
|
|
3225
4693
|
try {
|
|
3226
4694
|
const rulesyncMcp = await RulesyncMcp.fromFile({
|
|
3227
4695
|
validate: true
|
|
3228
4696
|
});
|
|
3229
|
-
const relativePathFromCwd =
|
|
4697
|
+
const relativePathFromCwd = join16(
|
|
3230
4698
|
rulesyncMcp.getRelativeDirPath(),
|
|
3231
4699
|
rulesyncMcp.getRelativeFilePath()
|
|
3232
4700
|
);
|
|
@@ -3264,7 +4732,7 @@ async function putMcpFile({ content }) {
|
|
|
3264
4732
|
const paths = RulesyncMcp.getSettablePaths();
|
|
3265
4733
|
const relativeDirPath = paths.recommended.relativeDirPath;
|
|
3266
4734
|
const relativeFilePath = paths.recommended.relativeFilePath;
|
|
3267
|
-
const fullPath =
|
|
4735
|
+
const fullPath = join16(baseDir, relativeDirPath, relativeFilePath);
|
|
3268
4736
|
const rulesyncMcp = new RulesyncMcp({
|
|
3269
4737
|
baseDir,
|
|
3270
4738
|
relativeDirPath,
|
|
@@ -3272,9 +4740,9 @@ async function putMcpFile({ content }) {
|
|
|
3272
4740
|
fileContent: content,
|
|
3273
4741
|
validate: true
|
|
3274
4742
|
});
|
|
3275
|
-
await ensureDir(
|
|
4743
|
+
await ensureDir(join16(baseDir, relativeDirPath));
|
|
3276
4744
|
await writeFileContent(fullPath, content);
|
|
3277
|
-
const relativePathFromCwd =
|
|
4745
|
+
const relativePathFromCwd = join16(relativeDirPath, relativeFilePath);
|
|
3278
4746
|
return {
|
|
3279
4747
|
relativePathFromCwd,
|
|
3280
4748
|
content: rulesyncMcp.getFileContent()
|
|
@@ -3292,15 +4760,15 @@ async function deleteMcpFile() {
|
|
|
3292
4760
|
try {
|
|
3293
4761
|
const baseDir = process.cwd();
|
|
3294
4762
|
const paths = RulesyncMcp.getSettablePaths();
|
|
3295
|
-
const recommendedPath =
|
|
4763
|
+
const recommendedPath = join16(
|
|
3296
4764
|
baseDir,
|
|
3297
4765
|
paths.recommended.relativeDirPath,
|
|
3298
4766
|
paths.recommended.relativeFilePath
|
|
3299
4767
|
);
|
|
3300
|
-
const legacyPath =
|
|
4768
|
+
const legacyPath = join16(baseDir, paths.legacy.relativeDirPath, paths.legacy.relativeFilePath);
|
|
3301
4769
|
await removeFile(recommendedPath);
|
|
3302
4770
|
await removeFile(legacyPath);
|
|
3303
|
-
const relativePathFromCwd =
|
|
4771
|
+
const relativePathFromCwd = join16(
|
|
3304
4772
|
paths.recommended.relativeDirPath,
|
|
3305
4773
|
paths.recommended.relativeFilePath
|
|
3306
4774
|
);
|
|
@@ -3317,11 +4785,11 @@ async function deleteMcpFile() {
|
|
|
3317
4785
|
}
|
|
3318
4786
|
}
|
|
3319
4787
|
var mcpToolSchemas = {
|
|
3320
|
-
getMcpFile:
|
|
3321
|
-
putMcpFile:
|
|
3322
|
-
content:
|
|
4788
|
+
getMcpFile: z13.object({}),
|
|
4789
|
+
putMcpFile: z13.object({
|
|
4790
|
+
content: z13.string()
|
|
3323
4791
|
}),
|
|
3324
|
-
deleteMcpFile:
|
|
4792
|
+
deleteMcpFile: z13.object({})
|
|
3325
4793
|
};
|
|
3326
4794
|
var mcpTools = {
|
|
3327
4795
|
getMcpFile: {
|
|
@@ -3353,14 +4821,141 @@ var mcpTools = {
|
|
|
3353
4821
|
}
|
|
3354
4822
|
};
|
|
3355
4823
|
|
|
4824
|
+
// src/mcp/permissions.ts
|
|
4825
|
+
import { join as join17 } from "path";
|
|
4826
|
+
import { z as z14 } from "zod/mini";
|
|
4827
|
+
var maxPermissionsSizeBytes = 1024 * 1024;
|
|
4828
|
+
async function getPermissionsFile() {
|
|
4829
|
+
try {
|
|
4830
|
+
const rulesyncPermissions = await RulesyncPermissions.fromFile({
|
|
4831
|
+
validate: true
|
|
4832
|
+
});
|
|
4833
|
+
const relativePathFromCwd = join17(
|
|
4834
|
+
rulesyncPermissions.getRelativeDirPath(),
|
|
4835
|
+
rulesyncPermissions.getRelativeFilePath()
|
|
4836
|
+
);
|
|
4837
|
+
return {
|
|
4838
|
+
relativePathFromCwd,
|
|
4839
|
+
content: rulesyncPermissions.getFileContent()
|
|
4840
|
+
};
|
|
4841
|
+
} catch (error) {
|
|
4842
|
+
throw new Error(
|
|
4843
|
+
`Failed to read permissions file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4844
|
+
{
|
|
4845
|
+
cause: error
|
|
4846
|
+
}
|
|
4847
|
+
);
|
|
4848
|
+
}
|
|
4849
|
+
}
|
|
4850
|
+
async function putPermissionsFile({ content }) {
|
|
4851
|
+
if (content.length > maxPermissionsSizeBytes) {
|
|
4852
|
+
throw new Error(
|
|
4853
|
+
`Permissions file size ${content.length} bytes exceeds maximum ${maxPermissionsSizeBytes} bytes (1MB) for ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}`
|
|
4854
|
+
);
|
|
4855
|
+
}
|
|
4856
|
+
try {
|
|
4857
|
+
JSON.parse(content);
|
|
4858
|
+
} catch (error) {
|
|
4859
|
+
throw new Error(
|
|
4860
|
+
`Invalid JSON format in permissions file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4861
|
+
{
|
|
4862
|
+
cause: error
|
|
4863
|
+
}
|
|
4864
|
+
);
|
|
4865
|
+
}
|
|
4866
|
+
try {
|
|
4867
|
+
const baseDir = process.cwd();
|
|
4868
|
+
const paths = RulesyncPermissions.getSettablePaths();
|
|
4869
|
+
const relativeDirPath = paths.relativeDirPath;
|
|
4870
|
+
const relativeFilePath = paths.relativeFilePath;
|
|
4871
|
+
const fullPath = join17(baseDir, relativeDirPath, relativeFilePath);
|
|
4872
|
+
const rulesyncPermissions = new RulesyncPermissions({
|
|
4873
|
+
baseDir,
|
|
4874
|
+
relativeDirPath,
|
|
4875
|
+
relativeFilePath,
|
|
4876
|
+
fileContent: content,
|
|
4877
|
+
validate: true
|
|
4878
|
+
});
|
|
4879
|
+
await ensureDir(join17(baseDir, relativeDirPath));
|
|
4880
|
+
await writeFileContent(fullPath, content);
|
|
4881
|
+
const relativePathFromCwd = join17(relativeDirPath, relativeFilePath);
|
|
4882
|
+
return {
|
|
4883
|
+
relativePathFromCwd,
|
|
4884
|
+
content: rulesyncPermissions.getFileContent()
|
|
4885
|
+
};
|
|
4886
|
+
} catch (error) {
|
|
4887
|
+
throw new Error(
|
|
4888
|
+
`Failed to write permissions file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4889
|
+
{
|
|
4890
|
+
cause: error
|
|
4891
|
+
}
|
|
4892
|
+
);
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
async function deletePermissionsFile() {
|
|
4896
|
+
try {
|
|
4897
|
+
const baseDir = process.cwd();
|
|
4898
|
+
const paths = RulesyncPermissions.getSettablePaths();
|
|
4899
|
+
const filePath = join17(baseDir, paths.relativeDirPath, paths.relativeFilePath);
|
|
4900
|
+
await removeFile(filePath);
|
|
4901
|
+
const relativePathFromCwd = join17(paths.relativeDirPath, paths.relativeFilePath);
|
|
4902
|
+
return {
|
|
4903
|
+
relativePathFromCwd
|
|
4904
|
+
};
|
|
4905
|
+
} catch (error) {
|
|
4906
|
+
throw new Error(
|
|
4907
|
+
`Failed to delete permissions file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}): ${formatError(error)}`,
|
|
4908
|
+
{
|
|
4909
|
+
cause: error
|
|
4910
|
+
}
|
|
4911
|
+
);
|
|
4912
|
+
}
|
|
4913
|
+
}
|
|
4914
|
+
var permissionsToolSchemas = {
|
|
4915
|
+
getPermissionsFile: z14.object({}),
|
|
4916
|
+
putPermissionsFile: z14.object({
|
|
4917
|
+
content: z14.string()
|
|
4918
|
+
}),
|
|
4919
|
+
deletePermissionsFile: z14.object({})
|
|
4920
|
+
};
|
|
4921
|
+
var permissionsTools = {
|
|
4922
|
+
getPermissionsFile: {
|
|
4923
|
+
name: "getPermissionsFile",
|
|
4924
|
+
description: `Get the permissions configuration file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}).`,
|
|
4925
|
+
parameters: permissionsToolSchemas.getPermissionsFile,
|
|
4926
|
+
execute: async () => {
|
|
4927
|
+
const result = await getPermissionsFile();
|
|
4928
|
+
return JSON.stringify(result, null, 2);
|
|
4929
|
+
}
|
|
4930
|
+
},
|
|
4931
|
+
putPermissionsFile: {
|
|
4932
|
+
name: "putPermissionsFile",
|
|
4933
|
+
description: "Create or update the permissions configuration file (upsert operation). content parameter is required and must be valid JSON.",
|
|
4934
|
+
parameters: permissionsToolSchemas.putPermissionsFile,
|
|
4935
|
+
execute: async (args) => {
|
|
4936
|
+
const result = await putPermissionsFile({ content: args.content });
|
|
4937
|
+
return JSON.stringify(result, null, 2);
|
|
4938
|
+
}
|
|
4939
|
+
},
|
|
4940
|
+
deletePermissionsFile: {
|
|
4941
|
+
name: "deletePermissionsFile",
|
|
4942
|
+
description: "Delete the permissions configuration file.",
|
|
4943
|
+
parameters: permissionsToolSchemas.deletePermissionsFile,
|
|
4944
|
+
execute: async () => {
|
|
4945
|
+
const result = await deletePermissionsFile();
|
|
4946
|
+
return JSON.stringify(result, null, 2);
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
};
|
|
4950
|
+
|
|
3356
4951
|
// src/mcp/rules.ts
|
|
3357
|
-
import { basename as
|
|
3358
|
-
import { z as
|
|
4952
|
+
import { basename as basename3, join as join18 } from "path";
|
|
4953
|
+
import { z as z15 } from "zod/mini";
|
|
3359
4954
|
var logger2 = new ConsoleLogger({ verbose: false, silent: true });
|
|
3360
4955
|
var maxRuleSizeBytes = 1024 * 1024;
|
|
3361
4956
|
var maxRulesCount = 1e3;
|
|
3362
4957
|
async function listRules() {
|
|
3363
|
-
const rulesDir =
|
|
4958
|
+
const rulesDir = join18(process.cwd(), RULESYNC_RULES_RELATIVE_DIR_PATH);
|
|
3364
4959
|
try {
|
|
3365
4960
|
const files = await listDirectoryFiles(rulesDir);
|
|
3366
4961
|
const mdFiles = files.filter((file) => file.endsWith(".md"));
|
|
@@ -3373,7 +4968,7 @@ async function listRules() {
|
|
|
3373
4968
|
});
|
|
3374
4969
|
const frontmatter = rule.getFrontmatter();
|
|
3375
4970
|
return {
|
|
3376
|
-
relativePathFromCwd:
|
|
4971
|
+
relativePathFromCwd: join18(RULESYNC_RULES_RELATIVE_DIR_PATH, file),
|
|
3377
4972
|
frontmatter
|
|
3378
4973
|
};
|
|
3379
4974
|
} catch (error) {
|
|
@@ -3395,14 +4990,14 @@ async function getRule({ relativePathFromCwd }) {
|
|
|
3395
4990
|
relativePath: relativePathFromCwd,
|
|
3396
4991
|
intendedRootDir: process.cwd()
|
|
3397
4992
|
});
|
|
3398
|
-
const filename =
|
|
4993
|
+
const filename = basename3(relativePathFromCwd);
|
|
3399
4994
|
try {
|
|
3400
4995
|
const rule = await RulesyncRule.fromFile({
|
|
3401
4996
|
relativeFilePath: filename,
|
|
3402
4997
|
validate: true
|
|
3403
4998
|
});
|
|
3404
4999
|
return {
|
|
3405
|
-
relativePathFromCwd:
|
|
5000
|
+
relativePathFromCwd: join18(RULESYNC_RULES_RELATIVE_DIR_PATH, filename),
|
|
3406
5001
|
frontmatter: rule.getFrontmatter(),
|
|
3407
5002
|
body: rule.getBody()
|
|
3408
5003
|
};
|
|
@@ -3421,7 +5016,7 @@ async function putRule({
|
|
|
3421
5016
|
relativePath: relativePathFromCwd,
|
|
3422
5017
|
intendedRootDir: process.cwd()
|
|
3423
5018
|
});
|
|
3424
|
-
const filename =
|
|
5019
|
+
const filename = basename3(relativePathFromCwd);
|
|
3425
5020
|
const estimatedSize = JSON.stringify(frontmatter).length + body.length;
|
|
3426
5021
|
if (estimatedSize > maxRuleSizeBytes) {
|
|
3427
5022
|
throw new Error(
|
|
@@ -3431,7 +5026,7 @@ async function putRule({
|
|
|
3431
5026
|
try {
|
|
3432
5027
|
const existingRules = await listRules();
|
|
3433
5028
|
const isUpdate = existingRules.some(
|
|
3434
|
-
(rule2) => rule2.relativePathFromCwd ===
|
|
5029
|
+
(rule2) => rule2.relativePathFromCwd === join18(RULESYNC_RULES_RELATIVE_DIR_PATH, filename)
|
|
3435
5030
|
);
|
|
3436
5031
|
if (!isUpdate && existingRules.length >= maxRulesCount) {
|
|
3437
5032
|
throw new Error(
|
|
@@ -3446,11 +5041,11 @@ async function putRule({
|
|
|
3446
5041
|
body,
|
|
3447
5042
|
validate: true
|
|
3448
5043
|
});
|
|
3449
|
-
const rulesDir =
|
|
5044
|
+
const rulesDir = join18(process.cwd(), RULESYNC_RULES_RELATIVE_DIR_PATH);
|
|
3450
5045
|
await ensureDir(rulesDir);
|
|
3451
5046
|
await writeFileContent(rule.getFilePath(), rule.getFileContent());
|
|
3452
5047
|
return {
|
|
3453
|
-
relativePathFromCwd:
|
|
5048
|
+
relativePathFromCwd: join18(RULESYNC_RULES_RELATIVE_DIR_PATH, filename),
|
|
3454
5049
|
frontmatter: rule.getFrontmatter(),
|
|
3455
5050
|
body: rule.getBody()
|
|
3456
5051
|
};
|
|
@@ -3465,12 +5060,12 @@ async function deleteRule({ relativePathFromCwd }) {
|
|
|
3465
5060
|
relativePath: relativePathFromCwd,
|
|
3466
5061
|
intendedRootDir: process.cwd()
|
|
3467
5062
|
});
|
|
3468
|
-
const filename =
|
|
3469
|
-
const fullPath =
|
|
5063
|
+
const filename = basename3(relativePathFromCwd);
|
|
5064
|
+
const fullPath = join18(process.cwd(), RULESYNC_RULES_RELATIVE_DIR_PATH, filename);
|
|
3470
5065
|
try {
|
|
3471
5066
|
await removeFile(fullPath);
|
|
3472
5067
|
return {
|
|
3473
|
-
relativePathFromCwd:
|
|
5068
|
+
relativePathFromCwd: join18(RULESYNC_RULES_RELATIVE_DIR_PATH, filename)
|
|
3474
5069
|
};
|
|
3475
5070
|
} catch (error) {
|
|
3476
5071
|
throw new Error(`Failed to delete rule file ${relativePathFromCwd}: ${formatError(error)}`, {
|
|
@@ -3479,23 +5074,23 @@ async function deleteRule({ relativePathFromCwd }) {
|
|
|
3479
5074
|
}
|
|
3480
5075
|
}
|
|
3481
5076
|
var ruleToolSchemas = {
|
|
3482
|
-
listRules:
|
|
3483
|
-
getRule:
|
|
3484
|
-
relativePathFromCwd:
|
|
5077
|
+
listRules: z15.object({}),
|
|
5078
|
+
getRule: z15.object({
|
|
5079
|
+
relativePathFromCwd: z15.string()
|
|
3485
5080
|
}),
|
|
3486
|
-
putRule:
|
|
3487
|
-
relativePathFromCwd:
|
|
5081
|
+
putRule: z15.object({
|
|
5082
|
+
relativePathFromCwd: z15.string(),
|
|
3488
5083
|
frontmatter: RulesyncRuleFrontmatterSchema,
|
|
3489
|
-
body:
|
|
5084
|
+
body: z15.string()
|
|
3490
5085
|
}),
|
|
3491
|
-
deleteRule:
|
|
3492
|
-
relativePathFromCwd:
|
|
5086
|
+
deleteRule: z15.object({
|
|
5087
|
+
relativePathFromCwd: z15.string()
|
|
3493
5088
|
})
|
|
3494
5089
|
};
|
|
3495
5090
|
var ruleTools = {
|
|
3496
5091
|
listRules: {
|
|
3497
5092
|
name: "listRules",
|
|
3498
|
-
description: `List all rules from ${
|
|
5093
|
+
description: `List all rules from ${join18(RULESYNC_RULES_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
|
|
3499
5094
|
parameters: ruleToolSchemas.listRules,
|
|
3500
5095
|
execute: async () => {
|
|
3501
5096
|
const rules = await listRules();
|
|
@@ -3537,8 +5132,8 @@ var ruleTools = {
|
|
|
3537
5132
|
};
|
|
3538
5133
|
|
|
3539
5134
|
// src/mcp/skills.ts
|
|
3540
|
-
import { basename as
|
|
3541
|
-
import { z as
|
|
5135
|
+
import { basename as basename4, dirname, join as join19 } from "path";
|
|
5136
|
+
import { z as z16 } from "zod/mini";
|
|
3542
5137
|
var logger3 = new ConsoleLogger({ verbose: false, silent: true });
|
|
3543
5138
|
var maxSkillSizeBytes = 1024 * 1024;
|
|
3544
5139
|
var maxSkillsCount = 1e3;
|
|
@@ -3555,19 +5150,19 @@ function mcpSkillFileToAiDirFile(file) {
|
|
|
3555
5150
|
};
|
|
3556
5151
|
}
|
|
3557
5152
|
function extractDirName(relativeDirPathFromCwd) {
|
|
3558
|
-
const dirName =
|
|
5153
|
+
const dirName = basename4(relativeDirPathFromCwd);
|
|
3559
5154
|
if (!dirName) {
|
|
3560
5155
|
throw new Error(`Invalid path: ${relativeDirPathFromCwd}`);
|
|
3561
5156
|
}
|
|
3562
5157
|
return dirName;
|
|
3563
5158
|
}
|
|
3564
5159
|
async function listSkills() {
|
|
3565
|
-
const skillsDir =
|
|
5160
|
+
const skillsDir = join19(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH);
|
|
3566
5161
|
try {
|
|
3567
|
-
const skillDirPaths = await findFilesByGlobs(
|
|
5162
|
+
const skillDirPaths = await findFilesByGlobs(join19(skillsDir, "*"), { type: "dir" });
|
|
3568
5163
|
const skills = await Promise.all(
|
|
3569
5164
|
skillDirPaths.map(async (dirPath) => {
|
|
3570
|
-
const dirName =
|
|
5165
|
+
const dirName = basename4(dirPath);
|
|
3571
5166
|
if (!dirName) return null;
|
|
3572
5167
|
try {
|
|
3573
5168
|
const skill = await RulesyncSkill.fromDir({
|
|
@@ -3575,7 +5170,7 @@ async function listSkills() {
|
|
|
3575
5170
|
});
|
|
3576
5171
|
const frontmatter = skill.getFrontmatter();
|
|
3577
5172
|
return {
|
|
3578
|
-
relativeDirPathFromCwd:
|
|
5173
|
+
relativeDirPathFromCwd: join19(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
|
|
3579
5174
|
frontmatter
|
|
3580
5175
|
};
|
|
3581
5176
|
} catch (error) {
|
|
@@ -3603,7 +5198,7 @@ async function getSkill({ relativeDirPathFromCwd }) {
|
|
|
3603
5198
|
dirName
|
|
3604
5199
|
});
|
|
3605
5200
|
return {
|
|
3606
|
-
relativeDirPathFromCwd:
|
|
5201
|
+
relativeDirPathFromCwd: join19(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
|
|
3607
5202
|
frontmatter: skill.getFrontmatter(),
|
|
3608
5203
|
body: skill.getBody(),
|
|
3609
5204
|
otherFiles: skill.getOtherFiles().map(aiDirFileToMcpSkillFile)
|
|
@@ -3637,7 +5232,7 @@ async function putSkill({
|
|
|
3637
5232
|
try {
|
|
3638
5233
|
const existingSkills = await listSkills();
|
|
3639
5234
|
const isUpdate = existingSkills.some(
|
|
3640
|
-
(skill2) => skill2.relativeDirPathFromCwd ===
|
|
5235
|
+
(skill2) => skill2.relativeDirPathFromCwd === join19(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName)
|
|
3641
5236
|
);
|
|
3642
5237
|
if (!isUpdate && existingSkills.length >= maxSkillsCount) {
|
|
3643
5238
|
throw new Error(
|
|
@@ -3654,9 +5249,9 @@ async function putSkill({
|
|
|
3654
5249
|
otherFiles: aiDirFiles,
|
|
3655
5250
|
validate: true
|
|
3656
5251
|
});
|
|
3657
|
-
const skillDirPath =
|
|
5252
|
+
const skillDirPath = join19(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName);
|
|
3658
5253
|
await ensureDir(skillDirPath);
|
|
3659
|
-
const skillFilePath =
|
|
5254
|
+
const skillFilePath = join19(skillDirPath, SKILL_FILE_NAME);
|
|
3660
5255
|
const skillFileContent = stringifyFrontmatter(body, frontmatter);
|
|
3661
5256
|
await writeFileContent(skillFilePath, skillFileContent);
|
|
3662
5257
|
for (const file of otherFiles) {
|
|
@@ -3664,15 +5259,15 @@ async function putSkill({
|
|
|
3664
5259
|
relativePath: file.name,
|
|
3665
5260
|
intendedRootDir: skillDirPath
|
|
3666
5261
|
});
|
|
3667
|
-
const filePath =
|
|
3668
|
-
const fileDir =
|
|
5262
|
+
const filePath = join19(skillDirPath, file.name);
|
|
5263
|
+
const fileDir = join19(skillDirPath, dirname(file.name));
|
|
3669
5264
|
if (fileDir !== skillDirPath) {
|
|
3670
5265
|
await ensureDir(fileDir);
|
|
3671
5266
|
}
|
|
3672
5267
|
await writeFileContent(filePath, file.body);
|
|
3673
5268
|
}
|
|
3674
5269
|
return {
|
|
3675
|
-
relativeDirPathFromCwd:
|
|
5270
|
+
relativeDirPathFromCwd: join19(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
|
|
3676
5271
|
frontmatter: skill.getFrontmatter(),
|
|
3677
5272
|
body: skill.getBody(),
|
|
3678
5273
|
otherFiles: skill.getOtherFiles().map(aiDirFileToMcpSkillFile)
|
|
@@ -3694,13 +5289,13 @@ async function deleteSkill({
|
|
|
3694
5289
|
intendedRootDir: process.cwd()
|
|
3695
5290
|
});
|
|
3696
5291
|
const dirName = extractDirName(relativeDirPathFromCwd);
|
|
3697
|
-
const skillDirPath =
|
|
5292
|
+
const skillDirPath = join19(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName);
|
|
3698
5293
|
try {
|
|
3699
5294
|
if (await directoryExists(skillDirPath)) {
|
|
3700
5295
|
await removeDirectory(skillDirPath);
|
|
3701
5296
|
}
|
|
3702
5297
|
return {
|
|
3703
|
-
relativeDirPathFromCwd:
|
|
5298
|
+
relativeDirPathFromCwd: join19(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName)
|
|
3704
5299
|
};
|
|
3705
5300
|
} catch (error) {
|
|
3706
5301
|
throw new Error(
|
|
@@ -3711,29 +5306,29 @@ async function deleteSkill({
|
|
|
3711
5306
|
);
|
|
3712
5307
|
}
|
|
3713
5308
|
}
|
|
3714
|
-
var McpSkillFileSchema =
|
|
3715
|
-
name:
|
|
3716
|
-
body:
|
|
5309
|
+
var McpSkillFileSchema = z16.object({
|
|
5310
|
+
name: z16.string(),
|
|
5311
|
+
body: z16.string()
|
|
3717
5312
|
});
|
|
3718
5313
|
var skillToolSchemas = {
|
|
3719
|
-
listSkills:
|
|
3720
|
-
getSkill:
|
|
3721
|
-
relativeDirPathFromCwd:
|
|
5314
|
+
listSkills: z16.object({}),
|
|
5315
|
+
getSkill: z16.object({
|
|
5316
|
+
relativeDirPathFromCwd: z16.string()
|
|
3722
5317
|
}),
|
|
3723
|
-
putSkill:
|
|
3724
|
-
relativeDirPathFromCwd:
|
|
5318
|
+
putSkill: z16.object({
|
|
5319
|
+
relativeDirPathFromCwd: z16.string(),
|
|
3725
5320
|
frontmatter: RulesyncSkillFrontmatterSchema,
|
|
3726
|
-
body:
|
|
3727
|
-
otherFiles:
|
|
5321
|
+
body: z16.string(),
|
|
5322
|
+
otherFiles: z16.optional(z16.array(McpSkillFileSchema))
|
|
3728
5323
|
}),
|
|
3729
|
-
deleteSkill:
|
|
3730
|
-
relativeDirPathFromCwd:
|
|
5324
|
+
deleteSkill: z16.object({
|
|
5325
|
+
relativeDirPathFromCwd: z16.string()
|
|
3731
5326
|
})
|
|
3732
5327
|
};
|
|
3733
5328
|
var skillTools = {
|
|
3734
5329
|
listSkills: {
|
|
3735
5330
|
name: "listSkills",
|
|
3736
|
-
description: `List all skills from ${
|
|
5331
|
+
description: `List all skills from ${join19(RULESYNC_SKILLS_RELATIVE_DIR_PATH, "*", SKILL_FILE_NAME)} with their frontmatter.`,
|
|
3737
5332
|
parameters: skillToolSchemas.listSkills,
|
|
3738
5333
|
execute: async () => {
|
|
3739
5334
|
const skills = await listSkills();
|
|
@@ -3776,13 +5371,13 @@ var skillTools = {
|
|
|
3776
5371
|
};
|
|
3777
5372
|
|
|
3778
5373
|
// src/mcp/subagents.ts
|
|
3779
|
-
import { basename as
|
|
3780
|
-
import { z as
|
|
5374
|
+
import { basename as basename5, join as join20 } from "path";
|
|
5375
|
+
import { z as z17 } from "zod/mini";
|
|
3781
5376
|
var logger4 = new ConsoleLogger({ verbose: false, silent: true });
|
|
3782
5377
|
var maxSubagentSizeBytes = 1024 * 1024;
|
|
3783
5378
|
var maxSubagentsCount = 1e3;
|
|
3784
5379
|
async function listSubagents() {
|
|
3785
|
-
const subagentsDir =
|
|
5380
|
+
const subagentsDir = join20(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
|
|
3786
5381
|
try {
|
|
3787
5382
|
const files = await listDirectoryFiles(subagentsDir);
|
|
3788
5383
|
const mdFiles = files.filter((file) => file.endsWith(".md"));
|
|
@@ -3795,7 +5390,7 @@ async function listSubagents() {
|
|
|
3795
5390
|
});
|
|
3796
5391
|
const frontmatter = subagent.getFrontmatter();
|
|
3797
5392
|
return {
|
|
3798
|
-
relativePathFromCwd:
|
|
5393
|
+
relativePathFromCwd: join20(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, file),
|
|
3799
5394
|
frontmatter
|
|
3800
5395
|
};
|
|
3801
5396
|
} catch (error) {
|
|
@@ -3819,14 +5414,14 @@ async function getSubagent({ relativePathFromCwd }) {
|
|
|
3819
5414
|
relativePath: relativePathFromCwd,
|
|
3820
5415
|
intendedRootDir: process.cwd()
|
|
3821
5416
|
});
|
|
3822
|
-
const filename =
|
|
5417
|
+
const filename = basename5(relativePathFromCwd);
|
|
3823
5418
|
try {
|
|
3824
5419
|
const subagent = await RulesyncSubagent.fromFile({
|
|
3825
5420
|
relativeFilePath: filename,
|
|
3826
5421
|
validate: true
|
|
3827
5422
|
});
|
|
3828
5423
|
return {
|
|
3829
|
-
relativePathFromCwd:
|
|
5424
|
+
relativePathFromCwd: join20(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
|
|
3830
5425
|
frontmatter: subagent.getFrontmatter(),
|
|
3831
5426
|
body: subagent.getBody()
|
|
3832
5427
|
};
|
|
@@ -3845,7 +5440,7 @@ async function putSubagent({
|
|
|
3845
5440
|
relativePath: relativePathFromCwd,
|
|
3846
5441
|
intendedRootDir: process.cwd()
|
|
3847
5442
|
});
|
|
3848
|
-
const filename =
|
|
5443
|
+
const filename = basename5(relativePathFromCwd);
|
|
3849
5444
|
const estimatedSize = JSON.stringify(frontmatter).length + body.length;
|
|
3850
5445
|
if (estimatedSize > maxSubagentSizeBytes) {
|
|
3851
5446
|
throw new Error(
|
|
@@ -3855,7 +5450,7 @@ async function putSubagent({
|
|
|
3855
5450
|
try {
|
|
3856
5451
|
const existingSubagents = await listSubagents();
|
|
3857
5452
|
const isUpdate = existingSubagents.some(
|
|
3858
|
-
(subagent2) => subagent2.relativePathFromCwd ===
|
|
5453
|
+
(subagent2) => subagent2.relativePathFromCwd === join20(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
|
|
3859
5454
|
);
|
|
3860
5455
|
if (!isUpdate && existingSubagents.length >= maxSubagentsCount) {
|
|
3861
5456
|
throw new Error(
|
|
@@ -3870,11 +5465,11 @@ async function putSubagent({
|
|
|
3870
5465
|
body,
|
|
3871
5466
|
validate: true
|
|
3872
5467
|
});
|
|
3873
|
-
const subagentsDir =
|
|
5468
|
+
const subagentsDir = join20(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
|
|
3874
5469
|
await ensureDir(subagentsDir);
|
|
3875
5470
|
await writeFileContent(subagent.getFilePath(), subagent.getFileContent());
|
|
3876
5471
|
return {
|
|
3877
|
-
relativePathFromCwd:
|
|
5472
|
+
relativePathFromCwd: join20(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
|
|
3878
5473
|
frontmatter: subagent.getFrontmatter(),
|
|
3879
5474
|
body: subagent.getBody()
|
|
3880
5475
|
};
|
|
@@ -3889,12 +5484,12 @@ async function deleteSubagent({ relativePathFromCwd }) {
|
|
|
3889
5484
|
relativePath: relativePathFromCwd,
|
|
3890
5485
|
intendedRootDir: process.cwd()
|
|
3891
5486
|
});
|
|
3892
|
-
const filename =
|
|
3893
|
-
const fullPath =
|
|
5487
|
+
const filename = basename5(relativePathFromCwd);
|
|
5488
|
+
const fullPath = join20(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename);
|
|
3894
5489
|
try {
|
|
3895
5490
|
await removeFile(fullPath);
|
|
3896
5491
|
return {
|
|
3897
|
-
relativePathFromCwd:
|
|
5492
|
+
relativePathFromCwd: join20(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
|
|
3898
5493
|
};
|
|
3899
5494
|
} catch (error) {
|
|
3900
5495
|
throw new Error(
|
|
@@ -3906,23 +5501,23 @@ async function deleteSubagent({ relativePathFromCwd }) {
|
|
|
3906
5501
|
}
|
|
3907
5502
|
}
|
|
3908
5503
|
var subagentToolSchemas = {
|
|
3909
|
-
listSubagents:
|
|
3910
|
-
getSubagent:
|
|
3911
|
-
relativePathFromCwd:
|
|
5504
|
+
listSubagents: z17.object({}),
|
|
5505
|
+
getSubagent: z17.object({
|
|
5506
|
+
relativePathFromCwd: z17.string()
|
|
3912
5507
|
}),
|
|
3913
|
-
putSubagent:
|
|
3914
|
-
relativePathFromCwd:
|
|
5508
|
+
putSubagent: z17.object({
|
|
5509
|
+
relativePathFromCwd: z17.string(),
|
|
3915
5510
|
frontmatter: RulesyncSubagentFrontmatterSchema,
|
|
3916
|
-
body:
|
|
5511
|
+
body: z17.string()
|
|
3917
5512
|
}),
|
|
3918
|
-
deleteSubagent:
|
|
3919
|
-
relativePathFromCwd:
|
|
5513
|
+
deleteSubagent: z17.object({
|
|
5514
|
+
relativePathFromCwd: z17.string()
|
|
3920
5515
|
})
|
|
3921
5516
|
};
|
|
3922
5517
|
var subagentTools = {
|
|
3923
5518
|
listSubagents: {
|
|
3924
5519
|
name: "listSubagents",
|
|
3925
|
-
description: `List all subagents from ${
|
|
5520
|
+
description: `List all subagents from ${join20(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
|
|
3926
5521
|
parameters: subagentToolSchemas.listSubagents,
|
|
3927
5522
|
execute: async () => {
|
|
3928
5523
|
const subagents = await listSubagents();
|
|
@@ -3964,31 +5559,33 @@ var subagentTools = {
|
|
|
3964
5559
|
};
|
|
3965
5560
|
|
|
3966
5561
|
// src/mcp/tools.ts
|
|
3967
|
-
var rulesyncFeatureSchema =
|
|
5562
|
+
var rulesyncFeatureSchema = z18.enum([
|
|
3968
5563
|
"rule",
|
|
3969
5564
|
"command",
|
|
3970
5565
|
"subagent",
|
|
3971
5566
|
"skill",
|
|
3972
5567
|
"ignore",
|
|
3973
5568
|
"mcp",
|
|
5569
|
+
"permissions",
|
|
5570
|
+
"hooks",
|
|
3974
5571
|
"generate",
|
|
3975
5572
|
"import"
|
|
3976
5573
|
]);
|
|
3977
|
-
var rulesyncOperationSchema =
|
|
3978
|
-
var skillFileSchema =
|
|
3979
|
-
name:
|
|
3980
|
-
body:
|
|
5574
|
+
var rulesyncOperationSchema = z18.enum(["list", "get", "put", "delete", "run"]);
|
|
5575
|
+
var skillFileSchema = z18.object({
|
|
5576
|
+
name: z18.string(),
|
|
5577
|
+
body: z18.string()
|
|
3981
5578
|
});
|
|
3982
|
-
var rulesyncToolSchema =
|
|
5579
|
+
var rulesyncToolSchema = z18.object({
|
|
3983
5580
|
feature: rulesyncFeatureSchema,
|
|
3984
5581
|
operation: rulesyncOperationSchema,
|
|
3985
|
-
targetPathFromCwd:
|
|
3986
|
-
frontmatter:
|
|
3987
|
-
body:
|
|
3988
|
-
otherFiles:
|
|
3989
|
-
content:
|
|
3990
|
-
generateOptions:
|
|
3991
|
-
importOptions:
|
|
5582
|
+
targetPathFromCwd: z18.optional(z18.string()),
|
|
5583
|
+
frontmatter: z18.optional(z18.unknown()),
|
|
5584
|
+
body: z18.optional(z18.string()),
|
|
5585
|
+
otherFiles: z18.optional(z18.array(skillFileSchema)),
|
|
5586
|
+
content: z18.optional(z18.string()),
|
|
5587
|
+
generateOptions: z18.optional(generateOptionsSchema),
|
|
5588
|
+
importOptions: z18.optional(importOptionsSchema)
|
|
3992
5589
|
});
|
|
3993
5590
|
var supportedOperationsByFeature = {
|
|
3994
5591
|
rule: ["list", "get", "put", "delete"],
|
|
@@ -3997,6 +5594,8 @@ var supportedOperationsByFeature = {
|
|
|
3997
5594
|
skill: ["list", "get", "put", "delete"],
|
|
3998
5595
|
ignore: ["get", "put", "delete"],
|
|
3999
5596
|
mcp: ["get", "put", "delete"],
|
|
5597
|
+
permissions: ["get", "put", "delete"],
|
|
5598
|
+
hooks: ["get", "put", "delete"],
|
|
4000
5599
|
generate: ["run"],
|
|
4001
5600
|
import: ["run"]
|
|
4002
5601
|
};
|
|
@@ -4046,7 +5645,7 @@ function ensureBody({ body, feature, operation }) {
|
|
|
4046
5645
|
}
|
|
4047
5646
|
var rulesyncTool = {
|
|
4048
5647
|
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.",
|
|
5648
|
+
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
5649
|
parameters: rulesyncToolSchema,
|
|
4051
5650
|
execute: async (args) => {
|
|
4052
5651
|
const parsed = rulesyncToolSchema.parse(args);
|
|
@@ -4163,6 +5762,30 @@ var rulesyncTool = {
|
|
|
4163
5762
|
}
|
|
4164
5763
|
return mcpTools.deleteMcpFile.execute();
|
|
4165
5764
|
}
|
|
5765
|
+
case "permissions": {
|
|
5766
|
+
if (parsed.operation === "get") {
|
|
5767
|
+
return permissionsTools.getPermissionsFile.execute();
|
|
5768
|
+
}
|
|
5769
|
+
if (parsed.operation === "put") {
|
|
5770
|
+
if (!parsed.content) {
|
|
5771
|
+
throw new Error("content is required for permissions put operation");
|
|
5772
|
+
}
|
|
5773
|
+
return permissionsTools.putPermissionsFile.execute({ content: parsed.content });
|
|
5774
|
+
}
|
|
5775
|
+
return permissionsTools.deletePermissionsFile.execute();
|
|
5776
|
+
}
|
|
5777
|
+
case "hooks": {
|
|
5778
|
+
if (parsed.operation === "get") {
|
|
5779
|
+
return hooksTools.getHooksFile.execute();
|
|
5780
|
+
}
|
|
5781
|
+
if (parsed.operation === "put") {
|
|
5782
|
+
if (!parsed.content) {
|
|
5783
|
+
throw new Error("content is required for hooks put operation");
|
|
5784
|
+
}
|
|
5785
|
+
return hooksTools.putHooksFile.execute({ content: parsed.content });
|
|
5786
|
+
}
|
|
5787
|
+
return hooksTools.deleteHooksFile.execute();
|
|
5788
|
+
}
|
|
4166
5789
|
case "generate": {
|
|
4167
5790
|
return generateTools.executeGenerate.execute(parsed.generateOptions ?? {});
|
|
4168
5791
|
}
|
|
@@ -4195,7 +5818,7 @@ async function mcpCommand(logger5, { version }) {
|
|
|
4195
5818
|
}
|
|
4196
5819
|
|
|
4197
5820
|
// src/cli/commands/resolve-gitignore-targets.ts
|
|
4198
|
-
import { join as
|
|
5821
|
+
import { join as join21 } from "path";
|
|
4199
5822
|
var resolveGitignoreTargets = async ({
|
|
4200
5823
|
cliTargets,
|
|
4201
5824
|
cwd = process.cwd()
|
|
@@ -4203,8 +5826,8 @@ var resolveGitignoreTargets = async ({
|
|
|
4203
5826
|
if (cliTargets !== void 0) {
|
|
4204
5827
|
return cliTargets;
|
|
4205
5828
|
}
|
|
4206
|
-
const baseConfigPath =
|
|
4207
|
-
const localConfigPath =
|
|
5829
|
+
const baseConfigPath = join21(cwd, RULESYNC_CONFIG_RELATIVE_FILE_PATH);
|
|
5830
|
+
const localConfigPath = join21(cwd, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
|
|
4208
5831
|
const [hasBase, hasLocal] = await Promise.all([
|
|
4209
5832
|
fileExists(baseConfigPath),
|
|
4210
5833
|
fileExists(localConfigPath)
|
|
@@ -4625,7 +6248,7 @@ function wrapCommand({
|
|
|
4625
6248
|
}
|
|
4626
6249
|
|
|
4627
6250
|
// src/cli/index.ts
|
|
4628
|
-
var getVersion = () => "8.
|
|
6251
|
+
var getVersion = () => "8.8.0";
|
|
4629
6252
|
function wrapCommand2(name, errorCode, handler) {
|
|
4630
6253
|
return wrapCommand({ name, errorCode, handler, getVersion });
|
|
4631
6254
|
}
|
|
@@ -4691,12 +6314,18 @@ var main = async () => {
|
|
|
4691
6314
|
await mcpCommand(logger5, { version });
|
|
4692
6315
|
})
|
|
4693
6316
|
);
|
|
4694
|
-
program.command("install").description("Install skills from declarative sources
|
|
6317
|
+
program.command("install").description("Install skills/primitives from declarative sources (rulesync.jsonc) or apm.yml").option(
|
|
6318
|
+
"--mode <mode>",
|
|
6319
|
+
`Install layout to produce (${INSTALL_MODES.join("|")}). Default: rulesync`
|
|
6320
|
+
).option("--update", "Force re-resolve all source refs, ignoring lockfile").option(
|
|
4695
6321
|
"--frozen",
|
|
4696
6322
|
"Fail if lockfile is missing or out of sync (for CI); fetches missing skills using locked refs"
|
|
4697
6323
|
).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
6324
|
wrapCommand2("install", "INSTALL_FAILED", async (logger5, options) => {
|
|
6325
|
+
const rawMode = options.mode;
|
|
6326
|
+
const mode = parseInstallMode(rawMode);
|
|
4699
6327
|
await installCommand(logger5, {
|
|
6328
|
+
mode,
|
|
4700
6329
|
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
4701
6330
|
update: options.update,
|
|
4702
6331
|
// eslint-disable-next-line no-type-assertion/no-type-assertion
|
|
@@ -4745,6 +6374,14 @@ var main = async () => {
|
|
|
4745
6374
|
);
|
|
4746
6375
|
program.parse();
|
|
4747
6376
|
};
|
|
6377
|
+
function parseInstallMode(raw) {
|
|
6378
|
+
if (raw === void 0) return void 0;
|
|
6379
|
+
const match = INSTALL_MODES.find((m) => m === raw);
|
|
6380
|
+
if (!match) {
|
|
6381
|
+
throw new Error(`Invalid --mode value "${raw}". Expected one of: ${INSTALL_MODES.join(", ")}.`);
|
|
6382
|
+
}
|
|
6383
|
+
return match;
|
|
6384
|
+
}
|
|
4748
6385
|
main().catch((error) => {
|
|
4749
6386
|
console.error(formatError(error));
|
|
4750
6387
|
process.exit(1);
|