rulesync 14.2.0 → 15.0.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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as createTempDirectory, A as RulesyncIgnore, At as RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH, B as ConfigFileSchema, Bt as RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH, C as RulesyncSubagentFrontmatterSchema, Ct as RULESYNC_AIIGNORE_FILE_NAME, D as RulesyncRuleFrontmatterSchema, Dt as RULESYNC_CONFIG_RELATIVE_FILE_PATH, E as RulesyncRule, Et as RULESYNC_COMMANDS_RELATIVE_DIR_PATH, F as RulesyncCheckFrontmatterSchema, Ft as RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH, G as fallbackLogger, Gt as RULESYNC_RELATIVE_DIR_PATH, H as findControlCharacter, Ht as RULESYNC_PERMISSIONS_JSONC_FILE_NAME, I as stringifyFrontmatter, It as RULESYNC_MCP_FILE_NAME, J as ErrorCodes, Jt as RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH, K as warnOnConflictingFlags, Kt as RULESYNC_RULES_RELATIVE_DIR_PATH, L as loadYaml, Lt as RULESYNC_MCP_JSONC_FILE_NAME, M as RulesyncCommand, Mt as RULESYNC_HOOKS_JSONC_FILE_NAME, N as RulesyncCommandFrontmatterSchema, Nt as RULESYNC_HOOKS_RELATIVE_FILE_PATH, O as RulesyncPermissions, Ot as RULESYNC_CONFIG_SCHEMA_URL, P as RulesyncCheck, Pt as RULESYNC_IGNORE_RELATIVE_FILE_PATH, Q as checkPathTraversal, Qt as ALL_FEATURES_WITH_WILDCARD, R as SKILL_FILE_NAME$1, Rt as RULESYNC_MCP_RELATIVE_FILE_PATH, S as RulesyncSubagent, St as MAX_FILE_SIZE, T as RulesyncSkillFrontmatterSchema, Tt as RULESYNC_CHECKS_RELATIVE_DIR_PATH, U as ConsoleLogger, Ut as RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH, V as SourceEntrySchema, Vt as RULESYNC_PERMISSIONS_FILE_NAME, W as JsonLogger, Wt as RULESYNC_PERMISSIONS_SCHEMA_URL, X as assertTreeContainsNoSymlinks, Xt as formatError, Y as assertDirectoryIfExists, Yt as RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, Z as assertWritablePathInsideRoot, Zt as ALL_FEATURES, _ as CLAUDECODE_MEMORIES_DIR_NAME, _t as toPosixPath, a as convertFromTool, at as getHomeDirectory, b as ChecksProcessor, bt as ALL_TOOL_TARGETS_WITH_WILDCARD, c as SkillsProcessor, ct as readFileContent, d as HooksProcessor, dt as removeDirectoryStrict, et as directoryExists, f as CommandsProcessor, ft as removeFile, g as CLAUDECODE_LOCAL_RULE_FILE_NAME, gt as runWithDirectoryRollback, h as CLAUDECODE_DIR, ht as resolvePath, i as getProcessorRegistryEntry, it as getFileSize, j as RulesyncHooks, jt as RULESYNC_HOOKS_FILE_NAME, k as RulesyncMcp, kt as RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH, l as McpProcessor, lt as readFileContentOrNull, m as CODEXCLI_DIR, mt as removeTempDirectory, n as checkRulesyncDirExists, nt as fileExists, o as RulesProcessor, ot as isSymlink, p as CODEXCLI_BASH_RULES_FILE_NAME, pt as removeFileStrict, q as CLIError, qt as RULESYNC_SKILLS_RELATIVE_DIR_PATH, r as generate, rt as findFilesByGlobs, s as SubagentsProcessor, st as listDirectoryFiles, t as importFromTool, tt as ensureDir, u as IgnoreProcessor, ut as removeDirectory, v as CLAUDECODE_SETTINGS_LOCAL_FILE_NAME, vt as writeFileContent, w as RulesyncSkill, wt as RULESYNC_AIIGNORE_RELATIVE_FILE_PATH, x as getLocalSkillDirNames, xt as ToolTargetSchema, y as CLAUDECODE_SKILLS_DIR_PATH, yt as ALL_TOOL_TARGETS, z as ConfigResolver, zt as RULESYNC_MCP_SCHEMA_URL } from "../import-CMCy95RI.js";
2
+ import { $ as assertDirectoryIfExists, $t as RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH, A as RulesyncMcp, At as RULESYNC_CHECKS_RELATIVE_DIR_PATH, B as stringifyFrontmatter, Bt as RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH, C as RulesyncSubagent, Ct as ALL_TOOL_TARGETS, D as RulesyncRule, Dt as MAX_FILE_SIZE, E as RulesyncSkillFrontmatterSchema, Et as ToolTargetSchema, F as parseJsonc, Ft as RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH, G as SourceEntrySchema, Gt as RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH, H as SKILL_FILE_NAME$1, Ht as RULESYNC_MCP_LEGACY_FILE_NAME, I as RulesyncCommand, It as RULESYNC_HOOKS_FILE_NAME, J as JsonLogger, Jt as RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH, K as findControlCharacter, Kt as RULESYNC_PERMISSIONS_FILE_NAME, L as RulesyncCommandFrontmatterSchema, Lt as RULESYNC_HOOKS_LEGACY_FILE_NAME, M as RulesyncHooks, Mt as RULESYNC_CONFIG_RELATIVE_FILE_PATH, N as getRulesyncSourceCandidates, Nt as RULESYNC_CONFIG_SCHEMA_URL, O as RulesyncRuleFrontmatterSchema, Ot as RULESYNC_AIIGNORE_FILE_NAME, P as resolveRulesyncSourceWritePath, Pt as RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH, Q as ErrorCodes, Qt as RULESYNC_SKILLS_RELATIVE_DIR_PATH, R as RulesyncCheck, Rt as RULESYNC_HOOKS_RELATIVE_FILE_PATH, S as getLocalSkillDirNames, St as writeFileContent, T as RulesyncSkill, Tt as PACKAGING_TOOL_TARGETS, U as ConfigResolver, Ut as RULESYNC_MCP_RELATIVE_FILE_PATH, V as loadYaml, Vt as RULESYNC_MCP_FILE_NAME, W as ConfigFileSchema, Wt as RULESYNC_MCP_SCHEMA_URL, X as warnOnConflictingFlags, Xt as RULESYNC_RELATIVE_DIR_PATH, Y as fallbackLogger, Yt as RULESYNC_PERMISSIONS_SCHEMA_URL, Z as CLIError, Zt as RULESYNC_RULES_RELATIVE_DIR_PATH, _ as CLAUDECODE_LOCAL_RULE_FILE_NAME, _t as removeFileStrict, a as convertFromTool, at as ensureDir, b as CLAUDECODE_SKILLS_DIR_PATH, bt as runWithDirectoryRollback, c as SubagentsProcessor, ct as getFileSize, d as IgnoreProcessor, dt as listDirectoryFiles, en as RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, et as assertTreeContainsNoSymlinks, f as HooksProcessor, ft as readFileContent, g as CLAUDECODE_DIR, gt as removeFile, h as CODEXCLI_DIR, ht as removeDirectoryStrict, i as getProcessorRegistryEntry, it as directoryExists, j as RulesyncIgnore, jt as RULESYNC_COMMANDS_RELATIVE_DIR_PATH, k as RulesyncPermissions, kt as RULESYNC_AIIGNORE_RELATIVE_FILE_PATH, l as SkillsProcessor, lt as getHomeDirectory, m as CODEXCLI_BASH_RULES_FILE_NAME, mt as removeDirectory, n as checkRulesyncDirExists, nn as ALL_FEATURES_WITH_WILDCARD, nt as checkPathTraversal, o as isPackagingToolTarget, ot as fileExists, p as CommandsProcessor, pt as readFileContentOrNull, q as ConsoleLogger, qt as RULESYNC_PERMISSIONS_LEGACY_FILE_NAME, r as generate, rn as formatError, rt as createTempDirectory, s as RulesProcessor, st as findFilesByGlobs, t as importFromTool, tn as ALL_FEATURES, tt as assertWritablePathInsideRoot, u as McpProcessor, ut as isSymlink, v as CLAUDECODE_MEMORIES_DIR_NAME, vt as removeTempDirectory, w as RulesyncSubagentFrontmatterSchema, wt as ALL_TOOL_TARGETS_WITH_WILDCARD, x as ChecksProcessor, xt as toPosixPath, y as CLAUDECODE_SETTINGS_LOCAL_FILE_NAME, yt as resolvePath, z as RulesyncCheckFrontmatterSchema, zt as RULESYNC_IGNORE_RELATIVE_FILE_PATH } from "../import-BuxzyDyH.js";
3
3
  import { Command } from "commander";
4
4
  import { nonnegative, optional, refine, z } from "zod/mini";
5
5
  import { cp, mkdtemp, realpath, rm } from "node:fs/promises";
@@ -276,8 +276,14 @@ function singletonTemplate(feature) {
276
276
  "src/**": "allow"
277
277
  },
278
278
  "read": {
279
- ".env": "deny"
279
+ ".env": "deny",
280
+ "credentials/**": "deny"
280
281
  }
282
+ },
283
+ "codexcli": {
284
+ "approval_policy": "on-request",
285
+ "approvals_reviewer": "auto_review",
286
+ "base_permission_profile": ":danger-full-access"
281
287
  }
282
288
  }
283
289
  `;
@@ -317,25 +323,19 @@ function createFeatureScaffold({ feature, name }) {
317
323
  });
318
324
  case "mcp": {
319
325
  const paths = RulesyncMcp.getSettablePaths();
320
- const relativeFilePath = join(paths.recommended.relativeDirPath, paths.recommended.relativeFilePath);
321
326
  return {
322
327
  feature,
323
- relativeFilePath,
324
- candidateRelativeFilePaths: [
325
- ...paths.jsonc ? [join(paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath)] : [],
326
- relativeFilePath,
327
- ...paths.legacy ? [join(paths.legacy.relativeDirPath, paths.legacy.relativeFilePath)] : []
328
- ],
328
+ relativeFilePath: join(paths.recommended.relativeDirPath, paths.recommended.relativeFilePath),
329
+ candidateRelativeFilePaths: getRulesyncSourceCandidates({ paths }).map((candidate) => join(candidate.relativeDirPath, candidate.relativeFilePath)),
329
330
  content: singletonTemplate(feature)
330
331
  };
331
332
  }
332
333
  case "hooks": {
333
334
  const paths = RulesyncHooks.getSettablePaths();
334
- const relativeFilePath = join(paths.relativeDirPath, paths.relativeFilePath);
335
335
  return {
336
336
  feature,
337
- relativeFilePath,
338
- candidateRelativeFilePaths: [...paths.jsonc ? [join(paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath)] : [], relativeFilePath],
337
+ relativeFilePath: join(paths.recommended.relativeDirPath, paths.recommended.relativeFilePath),
338
+ candidateRelativeFilePaths: getRulesyncSourceCandidates({ paths }).map((candidate) => join(candidate.relativeDirPath, candidate.relativeFilePath)),
339
339
  content: singletonTemplate(feature)
340
340
  };
341
341
  }
@@ -351,11 +351,10 @@ function createFeatureScaffold({ feature, name }) {
351
351
  }
352
352
  case "permissions": {
353
353
  const paths = RulesyncPermissions.getSettablePaths();
354
- const relativeFilePath = join(paths.relativeDirPath, paths.relativeFilePath);
355
354
  return {
356
355
  feature,
357
- relativeFilePath,
358
- candidateRelativeFilePaths: [...paths.jsonc ? [join(paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath)] : [], relativeFilePath],
356
+ relativeFilePath: join(paths.recommended.relativeDirPath, paths.recommended.relativeFilePath),
357
+ candidateRelativeFilePaths: getRulesyncSourceCandidates({ paths }).map((candidate) => join(candidate.relativeDirPath, candidate.relativeFilePath)),
359
358
  content: singletonTemplate(feature)
360
359
  };
361
360
  }
@@ -3795,6 +3794,8 @@ async function convertCommand(logger, options) {
3795
3794
  const fromTool = parseToolTarget$1(options.from ?? "", "source");
3796
3795
  const toToolsRaw = (options.to ?? []).map((t) => parseToolTarget$1(t, "destination"));
3797
3796
  const toTools = Array.from(new Set(toToolsRaw));
3797
+ const packagingTarget = [fromTool, ...toTools].find(isPackagingToolTarget);
3798
+ if (packagingTarget) throw new CLIError(`Plugin packaging target '${packagingTarget}' is not supported by convert. Use import --output-root and generate --output-roots with an explicit plugin directory.`, ErrorCodes.CONVERT_FAILED);
3798
3799
  if (toTools.includes(fromTool)) throw new CLIError(`Destination tools must not include the source tool '${fromTool}'. Converting a tool onto itself is likely a mistake and may cause lossy round-trips.`, ErrorCodes.CONVERT_FAILED);
3799
3800
  const config = await ConfigResolver.resolve({
3800
3801
  ...options,
@@ -3859,9 +3860,9 @@ const FEATURE_PATHS = {
3859
3860
  skills: ["skills"],
3860
3861
  checks: ["checks"],
3861
3862
  ignore: [RULESYNC_AIIGNORE_FILE_NAME],
3862
- mcp: [RULESYNC_MCP_FILE_NAME, RULESYNC_MCP_JSONC_FILE_NAME],
3863
- hooks: [RULESYNC_HOOKS_FILE_NAME, RULESYNC_HOOKS_JSONC_FILE_NAME],
3864
- permissions: [RULESYNC_PERMISSIONS_FILE_NAME, RULESYNC_PERMISSIONS_JSONC_FILE_NAME]
3863
+ mcp: [RULESYNC_MCP_FILE_NAME, RULESYNC_MCP_LEGACY_FILE_NAME],
3864
+ hooks: [RULESYNC_HOOKS_FILE_NAME, RULESYNC_HOOKS_LEGACY_FILE_NAME],
3865
+ permissions: [RULESYNC_PERMISSIONS_FILE_NAME, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME]
3865
3866
  };
3866
3867
  /**
3867
3868
  * Check if target is a tool target (not rulesync)
@@ -3999,10 +4000,11 @@ async function convertFetchedFilesToRulesync(params) {
3999
4000
  };
4000
4001
  }
4001
4002
  /**
4002
- * Resolve features from options, handling wildcard
4003
+ * Resolve features from options, defaulting to skills and handling wildcard.
4003
4004
  */
4004
4005
  function resolveFeatures(features) {
4005
- if (!features || features.length === 0 || features.includes("*")) return [...ALL_FEATURES];
4006
+ if (features === void 0) return ["skills"];
4007
+ if (features.includes("*")) return [...ALL_FEATURES];
4006
4008
  return features.filter((f) => ALL_FEATURES.includes(f));
4007
4009
  }
4008
4010
  /**
@@ -4821,6 +4823,7 @@ const ALL_GITIGNORE_ENTRIES = (() => {
4821
4823
  const seen = /* @__PURE__ */ new Set();
4822
4824
  const result = [];
4823
4825
  for (const tag of GITIGNORE_ENTRY_REGISTRY) {
4826
+ if (normalizeGitignoreEntryTargets(tag.target).every((target) => PACKAGING_TOOL_TARGETS.includes(target))) continue;
4824
4827
  if (seen.has(tag.entry)) continue;
4825
4828
  seen.add(tag.entry);
4826
4829
  result.push(tag.entry);
@@ -4830,14 +4833,13 @@ const ALL_GITIGNORE_ENTRIES = (() => {
4830
4833
  const isTargetSelected = (target, selectedTargets) => {
4831
4834
  const targets = normalizeGitignoreEntryTargets(target);
4832
4835
  if (targets.includes("common")) return true;
4833
- if (!selectedTargets || selectedTargets.length === 0) return true;
4834
- if (selectedTargets.includes("*")) return true;
4836
+ if (!selectedTargets || selectedTargets.length === 0 || selectedTargets.includes("*")) return targets.some((candidate) => selectedTargets?.includes(candidate) || !PACKAGING_TOOL_TARGETS.includes(candidate));
4835
4837
  return targets.some((candidate) => selectedTargets.includes(candidate));
4836
4838
  };
4837
4839
  const getSelectedGitignoreEntryTargets = (target, selectedTargets) => {
4838
4840
  const targets = normalizeGitignoreEntryTargets(target);
4839
4841
  if (targets.includes("common")) return ["common"];
4840
- if (!selectedTargets || selectedTargets.length === 0 || selectedTargets.includes("*")) return targets;
4842
+ if (!selectedTargets || selectedTargets.length === 0 || selectedTargets.includes("*")) return targets.filter((candidate) => selectedTargets?.includes(candidate) || !PACKAGING_TOOL_TARGETS.includes(candidate));
4841
4843
  return targets.filter((candidate) => selectedTargets.includes(candidate));
4842
4844
  };
4843
4845
  const isFeatureSelected = (feature, features) => {
@@ -5158,16 +5160,13 @@ async function createConfigFile() {
5158
5160
  await writeFileContent(path, JSON.stringify({
5159
5161
  $schema: RULESYNC_CONFIG_SCHEMA_URL,
5160
5162
  targets: [
5161
- "copilot",
5162
- "cursor",
5163
+ "codexcli",
5163
5164
  "claudecode",
5164
- "codexcli"
5165
+ "opencode"
5165
5166
  ],
5166
5167
  features: [
5167
5168
  "rules",
5168
- "ignore",
5169
5169
  "mcp",
5170
- "commands",
5171
5170
  "subagents",
5172
5171
  "skills",
5173
5172
  "hooks",
@@ -5195,10 +5194,6 @@ async function createSampleFiles() {
5195
5194
  name: "overview"
5196
5195
  }),
5197
5196
  createFeatureScaffold({ feature: "mcp" }),
5198
- createFeatureScaffold({
5199
- feature: "command",
5200
- name: "review-pr"
5201
- }),
5202
5197
  createFeatureScaffold({
5203
5198
  feature: "subagent",
5204
5199
  name: "planner"
@@ -5207,21 +5202,24 @@ async function createSampleFiles() {
5207
5202
  feature: "skill",
5208
5203
  name: "project-context"
5209
5204
  }),
5210
- createFeatureScaffold({ feature: "ignore" }),
5211
5205
  createFeatureScaffold({ feature: "hooks" }),
5212
5206
  createFeatureScaffold({ feature: "permissions" })
5213
5207
  ];
5214
5208
  const results = [];
5215
5209
  for (const sample of samples) {
5216
5210
  await ensureDir(dirname(sample.relativeFilePath));
5217
- results.push(await writeIfNotExists(sample.relativeFilePath, sample.content));
5211
+ results.push(await writeIfNotExists({
5212
+ path: sample.relativeFilePath,
5213
+ candidatePaths: sample.candidateRelativeFilePaths,
5214
+ content: sample.content
5215
+ }));
5218
5216
  }
5219
5217
  return results;
5220
5218
  }
5221
- async function writeIfNotExists(path, content) {
5222
- if (await fileExists(path)) return {
5219
+ async function writeIfNotExists({ path, candidatePaths, content }) {
5220
+ for (const candidatePath of candidatePaths) if (await fileExists(candidatePath)) return {
5223
5221
  created: false,
5224
- path
5222
+ path: candidatePath
5225
5223
  };
5226
5224
  await writeFileContent(path, content);
5227
5225
  return {
@@ -5257,7 +5255,7 @@ async function initCommand(logger) {
5257
5255
  }
5258
5256
  logger.success("rulesync initialized successfully!");
5259
5257
  logger.info("Next steps:");
5260
- logger.info(`1. Edit ${RULESYNC_RELATIVE_DIR_PATH}/**/*.md, ${RULESYNC_RELATIVE_DIR_PATH}/skills/*/${SKILL_FILE_NAME$1}, ${RULESYNC_MCP_RELATIVE_FILE_PATH}, ${RULESYNC_HOOKS_RELATIVE_FILE_PATH}, ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} and ${RULESYNC_AIIGNORE_RELATIVE_FILE_PATH}`);
5258
+ logger.info(`1. Edit ${RULESYNC_RELATIVE_DIR_PATH}/**/*.md, ${RULESYNC_RELATIVE_DIR_PATH}/skills/*/${SKILL_FILE_NAME$1}, ${RULESYNC_MCP_RELATIVE_FILE_PATH}, ${RULESYNC_HOOKS_RELATIVE_FILE_PATH} and ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}`);
5261
5259
  logger.info("2. Run 'rulesync generate' to create configuration files");
5262
5260
  }
5263
5261
  //#endregion
@@ -7235,17 +7233,16 @@ async function getHooksFile() {
7235
7233
  async function putHooksFile({ content }) {
7236
7234
  if (content.length > maxHooksSizeBytes) throw new Error(`Hooks file size ${content.length} bytes exceeds maximum ${maxHooksSizeBytes} bytes (1MB) for ${RULESYNC_HOOKS_RELATIVE_FILE_PATH}`);
7237
7235
  try {
7238
- JSON.parse(content);
7236
+ parseJsonc(content);
7239
7237
  } catch (error) {
7240
- throw new Error(`Invalid JSON format in hooks file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}): ${formatError(error)}`, { cause: error });
7238
+ throw new Error(`Invalid JSONC format in hooks file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}): ${formatError(error)}`, { cause: error });
7241
7239
  }
7242
7240
  try {
7243
7241
  const outputRoot = process.cwd();
7244
- const paths = RulesyncHooks.getSettablePaths();
7245
- const jsoncRelativePath = join(paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath);
7246
- if (await fileExists(join(outputRoot, jsoncRelativePath))) throw new Error(`${jsoncRelativePath} exists and takes precedence over ${RULESYNC_HOOKS_RELATIVE_FILE_PATH}. Edit ${jsoncRelativePath} directly instead.`);
7247
- const relativeDirPath = paths.relativeDirPath;
7248
- const relativeFilePath = paths.relativeFilePath;
7242
+ const { relativeDirPath, relativeFilePath } = await resolveRulesyncSourceWritePath({
7243
+ outputRoot,
7244
+ paths: RulesyncHooks.getSettablePaths()
7245
+ });
7249
7246
  const fullPath = join(outputRoot, relativeDirPath, relativeFilePath);
7250
7247
  const rulesyncHooks = new RulesyncHooks({
7251
7248
  outputRoot,
@@ -7271,9 +7268,8 @@ async function deleteHooksFile() {
7271
7268
  try {
7272
7269
  const outputRoot = process.cwd();
7273
7270
  const paths = RulesyncHooks.getSettablePaths();
7274
- await removeFile(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
7275
- await removeFile(join(outputRoot, paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath));
7276
- return { relativePathFromCwd: join(paths.relativeDirPath, paths.relativeFilePath) };
7271
+ for (const candidate of getRulesyncSourceCandidates({ paths })) await removeFile(join(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath));
7272
+ return { relativePathFromCwd: join(paths.recommended.relativeDirPath, paths.recommended.relativeFilePath) };
7277
7273
  } catch (error) {
7278
7274
  throw new Error(`Failed to delete hooks file (${RULESYNC_HOOKS_RELATIVE_FILE_PATH}): ${formatError(error)}`, { cause: error });
7279
7275
  }
@@ -7301,7 +7297,7 @@ const hooksTools = {
7301
7297
  },
7302
7298
  putHooksFile: {
7303
7299
  name: "putHooksFile",
7304
- description: "Create or update the hooks configuration file (upsert operation). content parameter is required and must be valid JSON.",
7300
+ description: "Create or update the hooks configuration file (upsert operation). content parameter is required and must be valid JSONC.",
7305
7301
  parameters: hooksToolSchemas.putHooksFile,
7306
7302
  execute: async (args) => {
7307
7303
  const result = await putHooksFile({ content: args.content });
@@ -7517,17 +7513,16 @@ async function getMcpFile() {
7517
7513
  async function putMcpFile({ content }) {
7518
7514
  if (content.length > maxMcpSizeBytes) throw new Error(`MCP file size ${content.length} bytes exceeds maximum ${maxMcpSizeBytes} bytes (1MB) for ${RULESYNC_MCP_RELATIVE_FILE_PATH}`);
7519
7515
  try {
7520
- JSON.parse(content);
7516
+ parseJsonc(content);
7521
7517
  } catch (error) {
7522
- throw new Error(`Invalid JSON format in MCP file (${RULESYNC_MCP_RELATIVE_FILE_PATH}): ${formatError(error)}`, { cause: error });
7518
+ throw new Error(`Invalid JSONC format in MCP file (${RULESYNC_MCP_RELATIVE_FILE_PATH}): ${formatError(error)}`, { cause: error });
7523
7519
  }
7524
7520
  try {
7525
7521
  const outputRoot = process.cwd();
7526
- const paths = RulesyncMcp.getSettablePaths();
7527
- const jsoncRelativePath = join(paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath);
7528
- if (await fileExists(join(outputRoot, jsoncRelativePath))) throw new Error(`${jsoncRelativePath} exists and takes precedence over ${RULESYNC_MCP_RELATIVE_FILE_PATH}. Edit ${jsoncRelativePath} directly instead.`);
7529
- const relativeDirPath = paths.recommended.relativeDirPath;
7530
- const relativeFilePath = paths.recommended.relativeFilePath;
7522
+ const { relativeDirPath, relativeFilePath } = await resolveRulesyncSourceWritePath({
7523
+ outputRoot,
7524
+ paths: RulesyncMcp.getSettablePaths()
7525
+ });
7531
7526
  const fullPath = join(outputRoot, relativeDirPath, relativeFilePath);
7532
7527
  const rulesyncMcp = new RulesyncMcp({
7533
7528
  outputRoot,
@@ -7553,11 +7548,7 @@ async function deleteMcpFile() {
7553
7548
  try {
7554
7549
  const outputRoot = process.cwd();
7555
7550
  const paths = RulesyncMcp.getSettablePaths();
7556
- const recommendedPath = join(outputRoot, paths.recommended.relativeDirPath, paths.recommended.relativeFilePath);
7557
- const legacyPath = join(outputRoot, paths.legacy.relativeDirPath, paths.legacy.relativeFilePath);
7558
- await removeFile(recommendedPath);
7559
- await removeFile(join(outputRoot, paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath));
7560
- await removeFile(legacyPath);
7551
+ for (const candidate of getRulesyncSourceCandidates({ paths })) await removeFile(join(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath));
7561
7552
  return { relativePathFromCwd: join(paths.recommended.relativeDirPath, paths.recommended.relativeFilePath) };
7562
7553
  } catch (error) {
7563
7554
  throw new Error(`Failed to delete MCP file (${RULESYNC_MCP_RELATIVE_FILE_PATH}): ${formatError(error)}`, { cause: error });
@@ -7586,7 +7577,7 @@ const mcpTools = {
7586
7577
  },
7587
7578
  putMcpFile: {
7588
7579
  name: "putMcpFile",
7589
- description: "Create or update the MCP configuration file (upsert operation). content parameter is required and must be valid JSON.",
7580
+ description: "Create or update the MCP configuration file (upsert operation). content parameter is required and must be valid JSONC.",
7590
7581
  parameters: mcpToolSchemas.putMcpFile,
7591
7582
  execute: async (args) => {
7592
7583
  const result = await putMcpFile({ content: args.content });
@@ -7626,17 +7617,16 @@ async function getPermissionsFile() {
7626
7617
  async function putPermissionsFile({ content }) {
7627
7618
  if (content.length > maxPermissionsSizeBytes) throw new Error(`Permissions file size ${content.length} bytes exceeds maximum ${maxPermissionsSizeBytes} bytes (1MB) for ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}`);
7628
7619
  try {
7629
- JSON.parse(content);
7620
+ parseJsonc(content);
7630
7621
  } catch (error) {
7631
- throw new Error(`Invalid JSON format in permissions file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}): ${formatError(error)}`, { cause: error });
7622
+ throw new Error(`Invalid JSONC format in permissions file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}): ${formatError(error)}`, { cause: error });
7632
7623
  }
7633
7624
  try {
7634
7625
  const outputRoot = process.cwd();
7635
- const paths = RulesyncPermissions.getSettablePaths();
7636
- const jsoncRelativePath = join(paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath);
7637
- if (await fileExists(join(outputRoot, jsoncRelativePath))) throw new Error(`${jsoncRelativePath} exists and takes precedence over ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}. Edit ${jsoncRelativePath} directly instead.`);
7638
- const relativeDirPath = paths.relativeDirPath;
7639
- const relativeFilePath = paths.relativeFilePath;
7626
+ const { relativeDirPath, relativeFilePath } = await resolveRulesyncSourceWritePath({
7627
+ outputRoot,
7628
+ paths: RulesyncPermissions.getSettablePaths()
7629
+ });
7640
7630
  const fullPath = join(outputRoot, relativeDirPath, relativeFilePath);
7641
7631
  const rulesyncPermissions = new RulesyncPermissions({
7642
7632
  outputRoot,
@@ -7662,9 +7652,8 @@ async function deletePermissionsFile() {
7662
7652
  try {
7663
7653
  const outputRoot = process.cwd();
7664
7654
  const paths = RulesyncPermissions.getSettablePaths();
7665
- await removeFile(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
7666
- await removeFile(join(outputRoot, paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath));
7667
- return { relativePathFromCwd: join(paths.relativeDirPath, paths.relativeFilePath) };
7655
+ for (const candidate of getRulesyncSourceCandidates({ paths })) await removeFile(join(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath));
7656
+ return { relativePathFromCwd: join(paths.recommended.relativeDirPath, paths.recommended.relativeFilePath) };
7668
7657
  } catch (error) {
7669
7658
  throw new Error(`Failed to delete permissions file (${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH}): ${formatError(error)}`, { cause: error });
7670
7659
  }
@@ -7692,7 +7681,7 @@ const permissionsTools = {
7692
7681
  },
7693
7682
  putPermissionsFile: {
7694
7683
  name: "putPermissionsFile",
7695
- description: "Create or update the permissions configuration file (upsert operation). content parameter is required and must be valid JSON.",
7684
+ description: "Create or update the permissions configuration file (upsert operation). content parameter is required and must be valid JSONC.",
7696
7685
  parameters: permissionsToolSchemas.putPermissionsFile,
7697
7686
  execute: async (args) => {
7698
7687
  const result = await putPermissionsFile({ content: args.content });
@@ -8989,8 +8978,9 @@ function wrapCommand$1({ name, errorCode, handler, getVersion, loggerFactory = c
8989
8978
  };
8990
8979
  }
8991
8980
  //#endregion
8992
- //#region src/cli/index.ts
8993
- const getVersion = () => "14.2.0";
8981
+ //#region src/cli/program.ts
8982
+ const getVersion = () => "15.0.0";
8983
+ const FEATURES_HELP = `${ALL_FEATURES.join(",")}; ignore is deprecated, use permissions`;
8994
8984
  function wrapCommand(name, errorCode, handler) {
8995
8985
  return wrapCommand$1({
8996
8986
  name,
@@ -8999,14 +8989,14 @@ function wrapCommand(name, errorCode, handler) {
8999
8989
  getVersion
9000
8990
  });
9001
8991
  }
9002
- const main = async () => {
8992
+ function createProgram() {
9003
8993
  const program = new Command();
9004
8994
  const version = getVersion();
9005
8995
  program.name("rulesync").description("Unified AI rules management CLI tool").version(version, "-v, --version", "Show version").option("-j, --json", "Output results as JSON");
9006
8996
  program.command("init").description("Initialize rulesync in current directory").option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").action(wrapCommand("init", "INIT_FAILED", async (logger) => {
9007
8997
  await initCommand(logger);
9008
8998
  }));
9009
- program.command("gitignore").description("Add generated files to .gitignore").option("-t, --targets <tools>", "Comma-separated list of tools to include (e.g., 'claudecode,copilot' or '*' for all)", parseCommaSeparatedList).option("-f, --features <features>", `Comma-separated list of features to include (${ALL_FEATURES.join(",")}) or '*' for all`, parseCommaSeparatedList).option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").action(wrapCommand("gitignore", "GITIGNORE_FAILED", async (logger, options) => {
8999
+ program.command("gitignore").description("Add generated files to .gitignore").option("-t, --targets <tools>", "Comma-separated list of tools to include (e.g., 'claudecode,copilot' or '*' for all)", parseCommaSeparatedList).option("-f, --features <features>", `Comma-separated list of features to include (${FEATURES_HELP}) or '*' for all`, parseCommaSeparatedList).option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").action(wrapCommand("gitignore", "GITIGNORE_FAILED", async (logger, options) => {
9010
9000
  const cliTargets = options.targets;
9011
9001
  const cliFeatures = options.features;
9012
9002
  const resolvedTargets = await resolveGitignoreTargets({ cliTargets });
@@ -9017,7 +9007,7 @@ const main = async () => {
9017
9007
  silent: options.silent
9018
9008
  });
9019
9009
  }));
9020
- program.command("add <source>").description("Add a Rulesync feature file or install a declarative rule or skill source").option("--name <name>", "Name for a rule, command, subagent, skill, or check scaffold").option("-f, --force", "Overwrite an existing scaffold file without prompting").option("--skills <skills>", "Comma-separated skill names to install", parseCommaSeparatedList).option("--rules <rules>", "Comma-separated rule names to install", parseCommaSeparatedList).option("--transport <transport>", "Source transport: github, git, or npm").option("-r, --ref <ref>", "Git ref, npm version, or npm dist-tag").option("-p, --path <path>", "Skills path within the source").option("--rules-path <path>", "Rules path within the source").option("--registry <url>", "npm-compatible registry URL").option("--token-env <name>", "Environment variable containing the npm registry token").option("--token <token>", "GitHub token for private repositories").option("-c, --config <path>", "Path to configuration file").option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").action(wrapCommand("add", "ADD_FAILED", async (logger, options, _globalOpts, positionalArgs) => {
9010
+ program.command("add <source>").description("Add a Rulesync feature file (ignore is deprecated; use permissions) or install a declarative rule or skill source").option("--name <name>", "Name for a rule, command, subagent, skill, or check scaffold").option("-f, --force", "Overwrite an existing scaffold file without prompting").option("--skills <skills>", "Comma-separated skill names to install", parseCommaSeparatedList).option("--rules <rules>", "Comma-separated rule names to install", parseCommaSeparatedList).option("--transport <transport>", "Source transport: github, git, or npm").option("-r, --ref <ref>", "Git ref, npm version, or npm dist-tag").option("-p, --path <path>", "Skills path within the source").option("--rules-path <path>", "Rules path within the source").option("--registry <url>", "npm-compatible registry URL").option("--token-env <name>", "Environment variable containing the npm registry token").option("--token <token>", "GitHub token for private repositories").option("-c, --config <path>", "Path to configuration file").option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").action(wrapCommand("add", "ADD_FAILED", async (logger, options, _globalOpts, positionalArgs) => {
9021
9011
  const source = positionalArgs[0];
9022
9012
  const addOptions = options;
9023
9013
  await addCommand(logger, {
@@ -9026,17 +9016,21 @@ const main = async () => {
9026
9016
  configPath: addOptions.config
9027
9017
  });
9028
9018
  }));
9029
- program.command("fetch <source>").description("Fetch files from a Git repository (GitHub/GitLab)").option("-t, --target <target>", "Target format to interpret files as (e.g., 'rulesync', 'claudecode'). Default: rulesync").option("-f, --features <features>", `Comma-separated list of features to fetch (${ALL_FEATURES.join(",")}) or '*' for all`, parseCommaSeparatedList).option("-r, --ref <ref>", "Branch, tag, or commit SHA to fetch from").option("-p, --path <path>", "Subdirectory path within the repository").option("-o, --output <dir>", "Output directory (default: .rulesync)").option("-c, --conflict <strategy>", "Conflict resolution strategy: skip, overwrite (default: overwrite)").option("--token <token>", "Git provider token for private repositories").option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").action(wrapCommand("fetch", "FETCH_FAILED", async (logger, options, _globalOpts, positionalArgs) => {
9019
+ program.command("fetch <source>").description("Fetch files from a Git repository (GitHub/GitLab)").option("-t, --target <target>", "Target format to interpret files as (e.g., 'rulesync', 'claudecode'). Default: rulesync").option("-f, --features <features>", `Comma-separated list of features to fetch (${FEATURES_HELP}) or '*' for all. Default: skills`, parseCommaSeparatedList).option("-r, --ref <ref>", "Branch, tag, or commit SHA to fetch from").option("-p, --path <path>", "Subdirectory path within the repository").option("-o, --output <dir>", "Output directory (default: .rulesync)").option("-c, --conflict <strategy>", "Conflict resolution strategy: skip, overwrite (default: overwrite)").option("--token <token>", "Git provider token for private repositories").option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").action(wrapCommand("fetch", "FETCH_FAILED", async (logger, options, _globalOpts, positionalArgs) => {
9030
9020
  const source = positionalArgs[0];
9031
9021
  await fetchCommand(logger, {
9032
9022
  ...options,
9033
9023
  source
9034
9024
  });
9035
9025
  }));
9036
- program.command("import").description("Import configurations from AI tools to rulesync format").option("-t, --targets <tool>", "Tool to import from (e.g., 'copilot', 'cursor', 'cline')", parseCommaSeparatedList).option("-f, --features <features>", `Comma-separated list of features to import (${ALL_FEATURES.join(",")}) or '*' for all`, parseCommaSeparatedList).option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").option("-g, --global", "Import for global(user scope) configuration files").action(wrapCommand("import", "IMPORT_FAILED", async (logger, options) => {
9037
- await importCommand(logger, options);
9026
+ program.command("import").description("Import configurations from AI tools to rulesync format").option("-t, --targets <tool>", "Tool to import from (e.g., 'copilot', 'cursor', 'cline')", parseCommaSeparatedList).option("-f, --features <features>", `Comma-separated list of features to import (${FEATURES_HELP}) or '*' for all`, parseCommaSeparatedList).option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").option("-g, --global", "Import for global(user scope) configuration files").option("-o, --output-root <path>", "Root directory containing the tool configuration to import").action(wrapCommand("import", "IMPORT_FAILED", async (logger, options) => {
9027
+ const { outputRoot, ...importOptions } = options;
9028
+ await importCommand(logger, {
9029
+ ...importOptions,
9030
+ outputRoots: outputRoot ? [outputRoot] : void 0
9031
+ });
9038
9032
  }));
9039
- program.command("convert").description("Convert configurations from one AI tool to other AI tools without writing .rulesync/ files").requiredOption("--from <tool>", "Source tool to convert from (e.g., 'cursor', 'claudecode')").requiredOption("--to <tools>", "Comma-separated list of destination tools (e.g., 'copilot,claudecode')", parseCommaSeparatedList).option("-f, --features <features>", `Comma-separated list of features to convert (${ALL_FEATURES.join(",")}) or '*' for all`, parseCommaSeparatedList).option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").option("-g, --global", "Convert for global(user scope) configuration files").option("--dry-run", "Dry run: show changes without writing files").action(wrapCommand("convert", "CONVERT_FAILED", async (logger, options) => {
9033
+ program.command("convert").description("Convert configurations from one AI tool to other AI tools without writing .rulesync/ files").requiredOption("--from <tool>", "Source tool to convert from (e.g., 'cursor', 'claudecode')").requiredOption("--to <tools>", "Comma-separated list of destination tools (e.g., 'copilot,claudecode')", parseCommaSeparatedList).option("-f, --features <features>", `Comma-separated list of features to convert (${FEATURES_HELP}) or '*' for all`, parseCommaSeparatedList).option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").option("-g, --global", "Convert for global(user scope) configuration files").option("--dry-run", "Dry run: show changes without writing files").action(wrapCommand("convert", "CONVERT_FAILED", async (logger, options) => {
9040
9034
  await convertCommand(logger, options);
9041
9035
  }));
9042
9036
  program.command("mcp").description("Start MCP server for rulesync").action(wrapCommand("mcp", "MCP_FAILED", async (logger, _options) => {
@@ -9054,20 +9048,25 @@ const main = async () => {
9054
9048
  silent: options.silent
9055
9049
  });
9056
9050
  }));
9057
- program.command("generate").description("Generate configuration files for AI tools").option("-t, --targets <tools>", "Comma-separated list of tools to generate for (e.g., 'copilot,cursor,cline' or '*' for all)", parseCommaSeparatedList).option("-f, --features <features>", `Comma-separated list of features to generate (${ALL_FEATURES.join(",")}) or '*' for all`, parseCommaSeparatedList).option("--delete", "Delete all existing files in output directories before generating").option("-o, --output-roots <paths>", "Output root directories to generate files into (comma-separated for multiple paths)", parseCommaSeparatedList).option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").option("-c, --config <path>", "Path to configuration file").option("-g, --global", "Generate for global(user scope) configuration files").option("--simulate-commands", "Generate simulated commands. This feature is only available for copilot, cursor and codexcli.").option("--simulate-subagents", "Generate simulated subagents. This feature is only available for copilot and codexcli.").option("--simulate-skills", "Generate simulated skills. This feature is only available for copilot, cursor and codexcli.").option("--input-root <path>", "Path to the directory containing .rulesync/ (parent of .rulesync/)").option("--dry-run", "Dry run: show changes without writing files").option("--check", "Check if files are up to date (exits with code 1 if changes needed)").action(wrapCommand("generate", "GENERATION_FAILED", async (logger, options) => {
9051
+ program.command("generate").description("Generate configuration files for AI tools").option("-t, --targets <tools>", "Comma-separated list of tools to generate for (e.g., 'copilot,cursor,cline' or '*' for all)", parseCommaSeparatedList).option("-f, --features <features>", `Comma-separated list of features to generate (${FEATURES_HELP}) or '*' for all`, parseCommaSeparatedList).option("--delete", "Delete all existing files in output directories before generating").option("-o, --output-roots <paths>", "Output root directories to generate files into (comma-separated for multiple paths)", parseCommaSeparatedList).option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").option("-c, --config <path>", "Path to configuration file").option("-g, --global", "Generate for global(user scope) configuration files").option("--simulate-commands", "Generate simulated commands. This feature is only available for copilot, cursor and codexcli.").option("--simulate-subagents", "Generate simulated subagents. This feature is only available for copilot and codexcli.").option("--simulate-skills", "Generate simulated skills. This feature is only available for copilot, cursor and codexcli.").option("--input-root <path>", "Path to the directory containing .rulesync/ (parent of .rulesync/)").option("--dry-run", "Dry run: show changes without writing files").option("--check", "Check if files are up to date (exits with code 1 if changes needed)").action(wrapCommand("generate", "GENERATION_FAILED", async (logger, options) => {
9058
9052
  await generateCommand(logger, options);
9059
9053
  }));
9060
9054
  program.command("update").description("Update rulesync to the latest version").option("--check", "Check for updates without installing").option("--force", "Force update even if already at latest version").option("--token <token>", "GitHub token for API access").option("-V, --verbose", "Verbose output").option("-s, --silent", "Suppress all output").action(wrapCommand("update", "UPDATE_FAILED", async (logger, options) => {
9061
9055
  await updateCommand(logger, version, options);
9062
9056
  }));
9063
- program.parse();
9064
- };
9057
+ return program;
9058
+ }
9065
9059
  function parseInstallMode(raw) {
9066
9060
  if (raw === void 0) return void 0;
9067
9061
  const match = INSTALL_MODES.find((m) => m === raw);
9068
9062
  if (!match) throw new Error(`Invalid --mode value "${raw}". Expected one of: ${INSTALL_MODES.join(", ")}.`);
9069
9063
  return match;
9070
9064
  }
9065
+ //#endregion
9066
+ //#region src/cli/index.ts
9067
+ async function main() {
9068
+ createProgram().parse();
9069
+ }
9071
9070
  main().catch((error) => {
9072
9071
  console.error(formatError(error));
9073
9072
  process.exit(1);