rulesync 7.15.2 → 7.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -71,7 +71,7 @@ See [Quick Start guide](https://dyoshikawa.github.io/rulesync/getting-started/qu
71
71
  | Gemini CLI | geminicli | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | 🎮 | ✅ 🌏 | ✅ 🌏 |
72
72
  | Goose | goose | ✅ 🌏 | ✅ | | | | | |
73
73
  | GitHub Copilot | copilot | ✅ 🌏 | | ✅ | ✅ | ✅ | ✅ | ✅ |
74
- | Cursor | cursor | ✅ | ✅ | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ |
74
+ | Cursor | cursor | ✅ | ✅ | 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ |
75
75
  | Factory Droid | factorydroid | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
76
76
  | OpenCode | opencode | ✅ 🌏 | | ✅ 🌏 🔧 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
77
77
  | Cline | cline | ✅ | ✅ | ✅ | ✅ 🌏 | | ✅ 🌏 | |
@@ -157,6 +157,8 @@ var RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join(
157
157
  var RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
158
158
  var RULESYNC_MCP_FILE_NAME = "mcp.json";
159
159
  var RULESYNC_HOOKS_FILE_NAME = "hooks.json";
160
+ var RULESYNC_CONFIG_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/config-schema.json";
161
+ var RULESYNC_MCP_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/mcp-schema.json";
160
162
  var MAX_FILE_SIZE = 10 * 1024 * 1024;
161
163
  var FETCH_CONCURRENCY_LIMIT = 10;
162
164
 
@@ -342,13 +344,22 @@ async function removeTempDirectory(tempDir) {
342
344
  // src/config/config.ts
343
345
  import { isAbsolute } from "path";
344
346
  import { minLength, optional, refine, z as z3 } from "zod/mini";
345
- function hasControlCharacters(value) {
347
+
348
+ // src/utils/validation.ts
349
+ function findControlCharacter(value) {
346
350
  for (let i = 0; i < value.length; i++) {
347
351
  const code = value.charCodeAt(i);
348
- if (code >= 0 && code <= 31 || code === 127) return true;
352
+ if (code >= 0 && code <= 31 || code === 127) {
353
+ return { position: i, hex: `0x${code.toString(16).padStart(2, "0")}` };
354
+ }
349
355
  }
350
- return false;
356
+ return null;
351
357
  }
358
+ function hasControlCharacters(value) {
359
+ return findControlCharacter(value) !== null;
360
+ }
361
+
362
+ // src/config/config.ts
352
363
  var SourceEntrySchema = z3.object({
353
364
  source: z3.string().check(minLength(1, "source must be a non-empty string")),
354
365
  skills: optional(z3.array(z3.string())),
@@ -728,6 +739,7 @@ import { join as join5 } from "path";
728
739
 
729
740
  // src/utils/frontmatter.ts
730
741
  import matter from "gray-matter";
742
+ import { dump, load } from "js-yaml";
731
743
  function isPlainObject(value) {
732
744
  if (value === null || typeof value !== "object") return false;
733
745
  const prototype = Object.getPrototypeOf(value);
@@ -766,8 +778,55 @@ function deepRemoveNullishObject(obj) {
766
778
  }
767
779
  return result;
768
780
  }
769
- function stringifyFrontmatter(body, frontmatter) {
770
- const cleanFrontmatter = deepRemoveNullishObject(frontmatter);
781
+ function deepFlattenStringsValue(value) {
782
+ if (value === null || value === void 0) {
783
+ return void 0;
784
+ }
785
+ if (typeof value === "string") {
786
+ return value.replace(/\n+/g, " ").trim();
787
+ }
788
+ if (Array.isArray(value)) {
789
+ const cleanedArray = value.map((item) => deepFlattenStringsValue(item)).filter((item) => item !== void 0);
790
+ return cleanedArray;
791
+ }
792
+ if (isPlainObject(value)) {
793
+ const result = {};
794
+ for (const [key, val] of Object.entries(value)) {
795
+ const cleaned = deepFlattenStringsValue(val);
796
+ if (cleaned !== void 0) {
797
+ result[key] = cleaned;
798
+ }
799
+ }
800
+ return result;
801
+ }
802
+ return value;
803
+ }
804
+ function deepFlattenStringsObject(obj) {
805
+ if (!obj || typeof obj !== "object") {
806
+ return {};
807
+ }
808
+ const result = {};
809
+ for (const [key, val] of Object.entries(obj)) {
810
+ const cleaned = deepFlattenStringsValue(val);
811
+ if (cleaned !== void 0) {
812
+ result[key] = cleaned;
813
+ }
814
+ }
815
+ return result;
816
+ }
817
+ function stringifyFrontmatter(body, frontmatter, options) {
818
+ const { avoidBlockScalars = false } = options ?? {};
819
+ const cleanFrontmatter = avoidBlockScalars ? deepFlattenStringsObject(frontmatter) : deepRemoveNullishObject(frontmatter);
820
+ if (avoidBlockScalars) {
821
+ return matter.stringify(body, cleanFrontmatter, {
822
+ engines: {
823
+ yaml: {
824
+ parse: (input) => load(input) ?? {},
825
+ stringify: (data) => dump(data, { lineWidth: -1 })
826
+ }
827
+ }
828
+ });
829
+ }
771
830
  return matter.stringify(body, cleanFrontmatter);
772
831
  }
773
832
  function parseFrontmatter(content, filePath) {
@@ -1886,7 +1945,7 @@ var CursorCommand = class _CursorCommand extends ToolCommand {
1886
1945
  }
1887
1946
  super({
1888
1947
  ...rest,
1889
- fileContent: stringifyFrontmatter(body, frontmatter)
1948
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
1890
1949
  });
1891
1950
  this.frontmatter = frontmatter;
1892
1951
  this.body = body;
@@ -5684,7 +5743,7 @@ import { z as z20 } from "zod/mini";
5684
5743
 
5685
5744
  // src/types/mcp.ts
5686
5745
  import { z as z19 } from "zod/mini";
5687
- var McpServerSchema = z19.object({
5746
+ var McpServerSchema = z19.looseObject({
5688
5747
  type: z19.optional(z19.enum(["stdio", "sse", "http"])),
5689
5748
  command: z19.optional(z19.union([z19.string(), z19.array(z19.string())])),
5690
5749
  args: z19.optional(z19.array(z19.string())),
@@ -5716,6 +5775,10 @@ var RulesyncMcpServerSchema = z20.extend(McpServerSchema, {
5716
5775
  var RulesyncMcpConfigSchema = z20.object({
5717
5776
  mcpServers: z20.record(z20.string(), RulesyncMcpServerSchema)
5718
5777
  });
5778
+ var RulesyncMcpFileSchema = z20.looseObject({
5779
+ $schema: z20.optional(z20.string()),
5780
+ ...RulesyncMcpConfigSchema.shape
5781
+ });
5719
5782
  var RulesyncMcp = class _RulesyncMcp extends RulesyncFile {
5720
5783
  json;
5721
5784
  constructor(params) {
@@ -5741,7 +5804,7 @@ var RulesyncMcp = class _RulesyncMcp extends RulesyncFile {
5741
5804
  };
5742
5805
  }
5743
5806
  validate() {
5744
- const result = RulesyncMcpConfigSchema.safeParse(this.json);
5807
+ const result = RulesyncMcpFileSchema.safeParse(this.json);
5745
5808
  if (!result.success) {
5746
5809
  return { success: false, error: result.error };
5747
5810
  }
@@ -5844,11 +5907,17 @@ var ToolMcp = class extends ToolFile {
5844
5907
  toRulesyncMcpDefault({
5845
5908
  fileContent = void 0
5846
5909
  } = {}) {
5910
+ const content = fileContent ?? this.fileContent;
5911
+ const { $schema: _, ...json } = JSON.parse(content);
5912
+ const withSchema = {
5913
+ $schema: RULESYNC_MCP_SCHEMA_URL,
5914
+ ...json
5915
+ };
5847
5916
  return new RulesyncMcp({
5848
5917
  baseDir: this.baseDir,
5849
5918
  relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
5850
- relativeFilePath: ".mcp.json",
5851
- fileContent: fileContent ?? this.fileContent
5919
+ relativeFilePath: RULESYNC_MCP_FILE_NAME,
5920
+ fileContent: JSON.stringify(withSchema, null, 2)
5852
5921
  });
5853
5922
  }
5854
5923
  static async fromFile(_params) {
@@ -6151,10 +6220,7 @@ var CodexcliMcp = class _CodexcliMcp extends ToolMcp {
6151
6220
  toRulesyncMcp() {
6152
6221
  const mcpServers = this.toml.mcp_servers ?? {};
6153
6222
  const converted = convertFromCodexFormat(mcpServers);
6154
- return new RulesyncMcp({
6155
- baseDir: this.baseDir,
6156
- relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
6157
- relativeFilePath: ".mcp.json",
6223
+ return this.toRulesyncMcpDefault({
6158
6224
  fileContent: JSON.stringify({ mcpServers: converted }, null, 2)
6159
6225
  });
6160
6226
  }
@@ -6312,12 +6378,26 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6312
6378
  json;
6313
6379
  constructor(params) {
6314
6380
  super(params);
6315
- this.json = this.fileContent !== void 0 ? JSON.parse(this.fileContent) : {};
6381
+ if (this.fileContent !== void 0) {
6382
+ try {
6383
+ this.json = JSON.parse(this.fileContent);
6384
+ } catch (error) {
6385
+ throw new Error(
6386
+ `Failed to parse Cursor MCP config at ${join46(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`,
6387
+ { cause: error }
6388
+ );
6389
+ }
6390
+ } else {
6391
+ this.json = {};
6392
+ }
6316
6393
  }
6317
6394
  getJson() {
6318
6395
  return this.json;
6319
6396
  }
6320
- static getSettablePaths() {
6397
+ isDeletable() {
6398
+ return !this.global;
6399
+ }
6400
+ static getSettablePaths(_options) {
6321
6401
  return {
6322
6402
  relativeDirPath: ".cursor",
6323
6403
  relativeFilePath: "mcp.json"
@@ -6325,41 +6405,62 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6325
6405
  }
6326
6406
  static async fromFile({
6327
6407
  baseDir = process.cwd(),
6328
- validate = true
6408
+ validate = true,
6409
+ global = false
6329
6410
  }) {
6330
- const fileContent = await readFileContent(
6331
- join46(
6332
- baseDir,
6333
- this.getSettablePaths().relativeDirPath,
6334
- this.getSettablePaths().relativeFilePath
6335
- )
6336
- );
6411
+ const paths = this.getSettablePaths({ global });
6412
+ const filePath = join46(baseDir, paths.relativeDirPath, paths.relativeFilePath);
6413
+ const fileContent = await readFileContentOrNull(filePath) ?? '{"mcpServers":{}}';
6414
+ let json;
6415
+ try {
6416
+ json = JSON.parse(fileContent);
6417
+ } catch (error) {
6418
+ throw new Error(
6419
+ `Failed to parse Cursor MCP config at ${join46(paths.relativeDirPath, paths.relativeFilePath)}: ${formatError(error)}`,
6420
+ { cause: error }
6421
+ );
6422
+ }
6423
+ const newJson = { ...json, mcpServers: json.mcpServers ?? {} };
6337
6424
  return new _CursorMcp({
6338
6425
  baseDir,
6339
- relativeDirPath: this.getSettablePaths().relativeDirPath,
6340
- relativeFilePath: this.getSettablePaths().relativeFilePath,
6341
- fileContent,
6342
- validate
6426
+ relativeDirPath: paths.relativeDirPath,
6427
+ relativeFilePath: paths.relativeFilePath,
6428
+ fileContent: JSON.stringify(newJson, null, 2),
6429
+ validate,
6430
+ global
6343
6431
  });
6344
6432
  }
6345
- static fromRulesyncMcp({
6433
+ static async fromRulesyncMcp({
6346
6434
  baseDir = process.cwd(),
6347
6435
  rulesyncMcp,
6348
- validate = true
6436
+ validate = true,
6437
+ global = false
6349
6438
  }) {
6350
- const json = rulesyncMcp.getJson();
6351
- const mcpServers = isMcpServers(json.mcpServers) ? json.mcpServers : {};
6439
+ const paths = this.getSettablePaths({ global });
6440
+ const fileContent = await readOrInitializeFileContent(
6441
+ join46(baseDir, paths.relativeDirPath, paths.relativeFilePath),
6442
+ JSON.stringify({ mcpServers: {} }, null, 2)
6443
+ );
6444
+ let json;
6445
+ try {
6446
+ json = JSON.parse(fileContent);
6447
+ } catch (error) {
6448
+ throw new Error(
6449
+ `Failed to parse Cursor MCP config at ${join46(paths.relativeDirPath, paths.relativeFilePath)}: ${formatError(error)}`,
6450
+ { cause: error }
6451
+ );
6452
+ }
6453
+ const rulesyncJson = rulesyncMcp.getJson();
6454
+ const mcpServers = isMcpServers(rulesyncJson.mcpServers) ? rulesyncJson.mcpServers : {};
6352
6455
  const transformedServers = convertEnvToCursorFormat(mcpServers);
6353
- const cursorConfig = {
6354
- mcpServers: transformedServers
6355
- };
6356
- const fileContent = JSON.stringify(cursorConfig, null, 2);
6456
+ const cursorConfig = { ...json, mcpServers: transformedServers };
6357
6457
  return new _CursorMcp({
6358
6458
  baseDir,
6359
- relativeDirPath: this.getSettablePaths().relativeDirPath,
6360
- relativeFilePath: this.getSettablePaths().relativeFilePath,
6361
- fileContent,
6362
- validate
6459
+ relativeDirPath: paths.relativeDirPath,
6460
+ relativeFilePath: paths.relativeFilePath,
6461
+ fileContent: JSON.stringify(cursorConfig, null, 2),
6462
+ validate,
6463
+ global
6363
6464
  });
6364
6465
  }
6365
6466
  toRulesyncMcp() {
@@ -6369,12 +6470,8 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6369
6470
  ...this.json,
6370
6471
  mcpServers: transformedServers
6371
6472
  };
6372
- return new RulesyncMcp({
6373
- baseDir: this.baseDir,
6374
- relativeDirPath: this.relativeDirPath,
6375
- relativeFilePath: "rulesync.mcp.json",
6376
- fileContent: JSON.stringify(transformedJson),
6377
- validate: true
6473
+ return this.toRulesyncMcpDefault({
6474
+ fileContent: JSON.stringify(transformedJson, null, 2)
6378
6475
  });
6379
6476
  }
6380
6477
  validate() {
@@ -6383,14 +6480,16 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6383
6480
  static forDeletion({
6384
6481
  baseDir = process.cwd(),
6385
6482
  relativeDirPath,
6386
- relativeFilePath
6483
+ relativeFilePath,
6484
+ global = false
6387
6485
  }) {
6388
6486
  return new _CursorMcp({
6389
6487
  baseDir,
6390
6488
  relativeDirPath,
6391
6489
  relativeFilePath,
6392
6490
  fileContent: "{}",
6393
- validate: false
6491
+ validate: false,
6492
+ global
6394
6493
  });
6395
6494
  }
6396
6495
  };
@@ -6450,13 +6549,7 @@ var FactorydroidMcp = class _FactorydroidMcp extends ToolMcp {
6450
6549
  });
6451
6550
  }
6452
6551
  toRulesyncMcp() {
6453
- return new RulesyncMcp({
6454
- baseDir: this.baseDir,
6455
- relativeDirPath: this.relativeDirPath,
6456
- relativeFilePath: "rulesync.mcp.json",
6457
- fileContent: JSON.stringify(this.json),
6458
- validate: true
6459
- });
6552
+ return this.toRulesyncMcpDefault();
6460
6553
  }
6461
6554
  validate() {
6462
6555
  return { success: true, error: null };
@@ -7219,7 +7312,7 @@ var toolMcpFactories = /* @__PURE__ */ new Map([
7219
7312
  class: CursorMcp,
7220
7313
  meta: {
7221
7314
  supportsProject: true,
7222
- supportsGlobal: false,
7315
+ supportsGlobal: true,
7223
7316
  supportsEnabledTools: false,
7224
7317
  supportsDisabledTools: false
7225
7318
  }
@@ -7932,9 +8025,15 @@ import { join as join59 } from "path";
7932
8025
  var DirFeatureProcessor = class {
7933
8026
  baseDir;
7934
8027
  dryRun;
7935
- constructor({ baseDir = process.cwd(), dryRun = false }) {
8028
+ avoidBlockScalars;
8029
+ constructor({
8030
+ baseDir = process.cwd(),
8031
+ dryRun = false,
8032
+ avoidBlockScalars = false
8033
+ }) {
7936
8034
  this.baseDir = baseDir;
7937
8035
  this.dryRun = dryRun;
8036
+ this.avoidBlockScalars = avoidBlockScalars;
7938
8037
  }
7939
8038
  /**
7940
8039
  * Return tool targets that this feature supports.
@@ -7960,7 +8059,9 @@ var DirFeatureProcessor = class {
7960
8059
  let mainFileContent;
7961
8060
  if (mainFile) {
7962
8061
  const mainFilePath = join59(dirPath, mainFile.name);
7963
- const content = stringifyFrontmatter(mainFile.body, mainFile.frontmatter);
8062
+ const content = stringifyFrontmatter(mainFile.body, mainFile.frontmatter, {
8063
+ avoidBlockScalars: this.avoidBlockScalars
8064
+ });
7964
8065
  mainFileContent = addTrailingNewline(content);
7965
8066
  const existingContent = await readFileContentOrNull(mainFilePath);
7966
8067
  if (existingContent !== mainFileContent) {
@@ -10699,7 +10800,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
10699
10800
  getFactory = defaultGetFactory4,
10700
10801
  dryRun = false
10701
10802
  }) {
10702
- super({ baseDir, dryRun });
10803
+ super({ baseDir, dryRun, avoidBlockScalars: toolTarget === "cursor" });
10703
10804
  const result = SkillsProcessorToolTargetSchema.safeParse(toolTarget);
10704
10805
  if (!result.success) {
10705
10806
  throw new Error(
@@ -11780,7 +11881,7 @@ var CursorSubagent = class _CursorSubagent extends ToolSubagent {
11780
11881
  ...cursorSection
11781
11882
  };
11782
11883
  const body = rulesyncSubagent.getBody();
11783
- const fileContent = stringifyFrontmatter(body, cursorFrontmatter);
11884
+ const fileContent = stringifyFrontmatter(body, cursorFrontmatter, { avoidBlockScalars: true });
11784
11885
  const paths = this.getSettablePaths({ global });
11785
11886
  return new _CursorSubagent({
11786
11887
  baseDir,
@@ -16894,6 +16995,8 @@ export {
16894
16995
  RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH,
16895
16996
  RULESYNC_MCP_FILE_NAME,
16896
16997
  RULESYNC_HOOKS_FILE_NAME,
16998
+ RULESYNC_CONFIG_SCHEMA_URL,
16999
+ RULESYNC_MCP_SCHEMA_URL,
16897
17000
  MAX_FILE_SIZE,
16898
17001
  FETCH_CONCURRENCY_LIMIT,
16899
17002
  formatError,
@@ -16915,6 +17018,7 @@ export {
16915
17018
  ALL_FEATURES,
16916
17019
  ALL_FEATURES_WITH_WILDCARD,
16917
17020
  ALL_TOOL_TARGETS,
17021
+ findControlCharacter,
16918
17022
  ConfigResolver,
16919
17023
  stringifyFrontmatter,
16920
17024
  RulesyncCommandFrontmatterSchema,