rulesync 8.6.0 → 8.7.0

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