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/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-ILMHM7BF.js";
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 = GITIGNORE_ENTRY_REGISTRY.map(
1348
- (tag) => tag.entry
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/sources.ts
1952
- import { join as join6, resolve, sep } from "path";
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 join4, relative } from "path";
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 = join4(tmpDir, skillsPath);
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 = join4(dir, name);
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 join5 } from "path";
2138
- import { optional, refine, z as z4 } from "zod/mini";
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 = z4.object({
2141
- integrity: z4.string()
3318
+ var LockedSkillSchema = z7.object({
3319
+ integrity: z7.string()
2142
3320
  });
2143
- var LockedSourceSchema = z4.object({
2144
- requestedRef: optional(z4.string()),
2145
- resolvedRef: z4.string().check(refine((v) => /^[0-9a-f]{40}$/.test(v), "resolvedRef must be a 40-character hex SHA")),
2146
- resolvedAt: optional(z4.string()),
2147
- skills: z4.record(z4.string(), LockedSkillSchema)
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 = z4.object({
2150
- lockfileVersion: z4.number(),
2151
- sources: z4.record(z4.string(), LockedSourceSchema)
3327
+ var SourcesLockSchema = z7.object({
3328
+ lockfileVersion: z7.number(),
3329
+ sources: z7.record(z7.string(), LockedSourceSchema)
2152
3330
  });
2153
- var LegacyLockedSourceSchema = z4.object({
2154
- resolvedRef: z4.string(),
2155
- skills: z4.array(z4.string())
3331
+ var LegacyLockedSourceSchema = z7.object({
3332
+ resolvedRef: z7.string(),
3333
+ skills: z7.array(z7.string())
2156
3334
  });
2157
- var LegacySourcesLockSchema = z4.object({
2158
- sources: z4.record(z4.string(), LegacyLockedSourceSchema)
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 = join5(params.baseDir, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH);
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 = join5(params.baseDir, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH);
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 = createHash("sha256");
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(join6(curatedDir, name))) {
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 = join6(curatedDir, prevSkill);
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: join6(curatedDir, skillName)
3614
+ intendedRootDir: join12(curatedDir, skillName)
2437
3615
  });
2438
- await writeFileContent(join6(curatedDir, skillName, file.relativePath), file.content);
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 = join6(baseDir, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
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
- const semaphore = new Semaphore2(FETCH_CONCURRENCY_LIMIT);
2543
- const fetchedSkills = {};
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: filteredDirs.map((d) => d.name),
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 = join6(baseDir, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH);
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 = /* @__PURE__ */ new Map();
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: process.cwd(),
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 z13 } from "zod/mini";
4109
+ import { z as z18 } from "zod/mini";
2769
4110
 
2770
4111
  // src/mcp/commands.ts
2771
- import { basename, join as join7 } from "path";
2772
- import { z as z5 } from "zod/mini";
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 = join7(process.cwd(), RULESYNC_COMMANDS_RELATIVE_DIR_PATH);
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: join7(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, file),
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 = basename(relativePathFromCwd);
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: join7(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename),
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 = basename(relativePathFromCwd);
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 === join7(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename)
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 = join7(process.cwd(), RULESYNC_COMMANDS_RELATIVE_DIR_PATH);
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: join7(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename),
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 = basename(relativePathFromCwd);
2887
- const fullPath = join7(process.cwd(), RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename);
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: join7(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, filename)
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: z5.object({}),
2901
- getCommand: z5.object({
2902
- relativePathFromCwd: z5.string()
4241
+ listCommands: z8.object({}),
4242
+ getCommand: z8.object({
4243
+ relativePathFromCwd: z8.string()
2903
4244
  }),
2904
- putCommand: z5.object({
2905
- relativePathFromCwd: z5.string(),
4245
+ putCommand: z8.object({
4246
+ relativePathFromCwd: z8.string(),
2906
4247
  frontmatter: RulesyncCommandFrontmatterSchema,
2907
- body: z5.string()
4248
+ body: z8.string()
2908
4249
  }),
2909
- deleteCommand: z5.object({
2910
- relativePathFromCwd: z5.string()
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 ${join7(RULESYNC_COMMANDS_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
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 z6 } from "zod/mini";
2959
- var generateOptionsSchema = z6.object({
2960
- targets: z6.optional(z6.array(z6.string())),
2961
- features: z6.optional(z6.array(z6.string())),
2962
- delete: z6.optional(z6.boolean()),
2963
- global: z6.optional(z6.boolean()),
2964
- simulateCommands: z6.optional(z6.boolean()),
2965
- simulateSubagents: z6.optional(z6.boolean()),
2966
- simulateSkills: z6.optional(z6.boolean())
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 join8 } from "path";
3046
- import { z as z7 } from "zod/mini";
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 = join8(process.cwd(), RULESYNC_AIIGNORE_RELATIVE_FILE_PATH);
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 = join8(process.cwd(), RULESYNC_AIIGNORE_RELATIVE_FILE_PATH);
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 = join8(process.cwd(), RULESYNC_AIIGNORE_RELATIVE_FILE_PATH);
3091
- const legacyIgnorePath = join8(process.cwd(), RULESYNC_IGNORE_RELATIVE_FILE_PATH);
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: z7.object({}),
3110
- putIgnoreFile: z7.object({
3111
- content: z7.string()
4577
+ getIgnoreFile: z11.object({}),
4578
+ putIgnoreFile: z11.object({
4579
+ content: z11.string()
3112
4580
  }),
3113
- deleteIgnoreFile: z7.object({})
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 z8 } from "zod/mini";
3147
- var importOptionsSchema = z8.object({
3148
- target: z8.string(),
3149
- features: z8.optional(z8.array(z8.string())),
3150
- global: z8.optional(z8.boolean())
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 join9 } from "path";
3222
- import { z as z9 } from "zod/mini";
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 = join9(
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 = join9(baseDir, relativeDirPath, relativeFilePath);
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(join9(baseDir, relativeDirPath));
4743
+ await ensureDir(join16(baseDir, relativeDirPath));
3276
4744
  await writeFileContent(fullPath, content);
3277
- const relativePathFromCwd = join9(relativeDirPath, relativeFilePath);
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 = join9(
4763
+ const recommendedPath = join16(
3296
4764
  baseDir,
3297
4765
  paths.recommended.relativeDirPath,
3298
4766
  paths.recommended.relativeFilePath
3299
4767
  );
3300
- const legacyPath = join9(baseDir, paths.legacy.relativeDirPath, paths.legacy.relativeFilePath);
4768
+ const legacyPath = join16(baseDir, paths.legacy.relativeDirPath, paths.legacy.relativeFilePath);
3301
4769
  await removeFile(recommendedPath);
3302
4770
  await removeFile(legacyPath);
3303
- const relativePathFromCwd = join9(
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: z9.object({}),
3321
- putMcpFile: z9.object({
3322
- content: z9.string()
4788
+ getMcpFile: z13.object({}),
4789
+ putMcpFile: z13.object({
4790
+ content: z13.string()
3323
4791
  }),
3324
- deleteMcpFile: z9.object({})
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 basename2, join as join10 } from "path";
3358
- import { z as z10 } from "zod/mini";
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 = join10(process.cwd(), RULESYNC_RULES_RELATIVE_DIR_PATH);
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: join10(RULESYNC_RULES_RELATIVE_DIR_PATH, file),
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 = basename2(relativePathFromCwd);
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: join10(RULESYNC_RULES_RELATIVE_DIR_PATH, filename),
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 = basename2(relativePathFromCwd);
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 === join10(RULESYNC_RULES_RELATIVE_DIR_PATH, filename)
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 = join10(process.cwd(), RULESYNC_RULES_RELATIVE_DIR_PATH);
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: join10(RULESYNC_RULES_RELATIVE_DIR_PATH, filename),
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 = basename2(relativePathFromCwd);
3469
- const fullPath = join10(process.cwd(), RULESYNC_RULES_RELATIVE_DIR_PATH, filename);
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: join10(RULESYNC_RULES_RELATIVE_DIR_PATH, filename)
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: z10.object({}),
3483
- getRule: z10.object({
3484
- relativePathFromCwd: z10.string()
5077
+ listRules: z15.object({}),
5078
+ getRule: z15.object({
5079
+ relativePathFromCwd: z15.string()
3485
5080
  }),
3486
- putRule: z10.object({
3487
- relativePathFromCwd: z10.string(),
5081
+ putRule: z15.object({
5082
+ relativePathFromCwd: z15.string(),
3488
5083
  frontmatter: RulesyncRuleFrontmatterSchema,
3489
- body: z10.string()
5084
+ body: z15.string()
3490
5085
  }),
3491
- deleteRule: z10.object({
3492
- relativePathFromCwd: z10.string()
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 ${join10(RULESYNC_RULES_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
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 basename3, dirname, join as join11 } from "path";
3541
- import { z as z11 } from "zod/mini";
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 = basename3(relativeDirPathFromCwd);
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 = join11(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH);
5160
+ const skillsDir = join19(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH);
3566
5161
  try {
3567
- const skillDirPaths = await findFilesByGlobs(join11(skillsDir, "*"), { type: "dir" });
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 = basename3(dirPath);
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: join11(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
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: join11(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
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 === join11(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName)
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 = join11(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName);
5252
+ const skillDirPath = join19(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName);
3658
5253
  await ensureDir(skillDirPath);
3659
- const skillFilePath = join11(skillDirPath, SKILL_FILE_NAME);
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 = join11(skillDirPath, file.name);
3668
- const fileDir = join11(skillDirPath, dirname(file.name));
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: join11(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName),
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 = join11(process.cwd(), RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName);
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: join11(RULESYNC_SKILLS_RELATIVE_DIR_PATH, dirName)
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 = z11.object({
3715
- name: z11.string(),
3716
- body: z11.string()
5309
+ var McpSkillFileSchema = z16.object({
5310
+ name: z16.string(),
5311
+ body: z16.string()
3717
5312
  });
3718
5313
  var skillToolSchemas = {
3719
- listSkills: z11.object({}),
3720
- getSkill: z11.object({
3721
- relativeDirPathFromCwd: z11.string()
5314
+ listSkills: z16.object({}),
5315
+ getSkill: z16.object({
5316
+ relativeDirPathFromCwd: z16.string()
3722
5317
  }),
3723
- putSkill: z11.object({
3724
- relativeDirPathFromCwd: z11.string(),
5318
+ putSkill: z16.object({
5319
+ relativeDirPathFromCwd: z16.string(),
3725
5320
  frontmatter: RulesyncSkillFrontmatterSchema,
3726
- body: z11.string(),
3727
- otherFiles: z11.optional(z11.array(McpSkillFileSchema))
5321
+ body: z16.string(),
5322
+ otherFiles: z16.optional(z16.array(McpSkillFileSchema))
3728
5323
  }),
3729
- deleteSkill: z11.object({
3730
- relativeDirPathFromCwd: z11.string()
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 ${join11(RULESYNC_SKILLS_RELATIVE_DIR_PATH, "*", SKILL_FILE_NAME)} with their frontmatter.`,
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 basename4, join as join12 } from "path";
3780
- import { z as z12 } from "zod/mini";
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 = join12(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
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: join12(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, file),
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 = basename4(relativePathFromCwd);
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: join12(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
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 = basename4(relativePathFromCwd);
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 === join12(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
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 = join12(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH);
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: join12(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename),
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 = basename4(relativePathFromCwd);
3893
- const fullPath = join12(process.cwd(), RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename);
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: join12(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, filename)
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: z12.object({}),
3910
- getSubagent: z12.object({
3911
- relativePathFromCwd: z12.string()
5504
+ listSubagents: z17.object({}),
5505
+ getSubagent: z17.object({
5506
+ relativePathFromCwd: z17.string()
3912
5507
  }),
3913
- putSubagent: z12.object({
3914
- relativePathFromCwd: z12.string(),
5508
+ putSubagent: z17.object({
5509
+ relativePathFromCwd: z17.string(),
3915
5510
  frontmatter: RulesyncSubagentFrontmatterSchema,
3916
- body: z12.string()
5511
+ body: z17.string()
3917
5512
  }),
3918
- deleteSubagent: z12.object({
3919
- relativePathFromCwd: z12.string()
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 ${join12(RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, "*.md")} with their frontmatter.`,
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 = z13.enum([
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 = z13.enum(["list", "get", "put", "delete", "run"]);
3978
- var skillFileSchema = z13.object({
3979
- name: z13.string(),
3980
- body: z13.string()
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 = z13.object({
5579
+ var rulesyncToolSchema = z18.object({
3983
5580
  feature: rulesyncFeatureSchema,
3984
5581
  operation: rulesyncOperationSchema,
3985
- targetPathFromCwd: z13.optional(z13.string()),
3986
- frontmatter: z13.optional(z13.unknown()),
3987
- body: z13.optional(z13.string()),
3988
- otherFiles: z13.optional(z13.array(skillFileSchema)),
3989
- content: z13.optional(z13.string()),
3990
- generateOptions: z13.optional(generateOptionsSchema),
3991
- importOptions: z13.optional(importOptionsSchema)
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 join13 } from "path";
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 = join13(cwd, RULESYNC_CONFIG_RELATIVE_FILE_PATH);
4207
- const localConfigPath = join13(cwd, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH);
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.6.0";
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 in rulesync.jsonc").option("--update", "Force re-resolve all source refs, ignoring lockfile").option(
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);