rulesync 7.15.2 → 7.16.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 | ✅ | ✅ | ✅ | ✅ 🌏 | | ✅ 🌏 | |
@@ -342,13 +342,22 @@ async function removeTempDirectory(tempDir) {
342
342
  // src/config/config.ts
343
343
  import { isAbsolute } from "path";
344
344
  import { minLength, optional, refine, z as z3 } from "zod/mini";
345
- function hasControlCharacters(value) {
345
+
346
+ // src/utils/validation.ts
347
+ function findControlCharacter(value) {
346
348
  for (let i = 0; i < value.length; i++) {
347
349
  const code = value.charCodeAt(i);
348
- if (code >= 0 && code <= 31 || code === 127) return true;
350
+ if (code >= 0 && code <= 31 || code === 127) {
351
+ return { position: i, hex: `0x${code.toString(16).padStart(2, "0")}` };
352
+ }
349
353
  }
350
- return false;
354
+ return null;
351
355
  }
356
+ function hasControlCharacters(value) {
357
+ return findControlCharacter(value) !== null;
358
+ }
359
+
360
+ // src/config/config.ts
352
361
  var SourceEntrySchema = z3.object({
353
362
  source: z3.string().check(minLength(1, "source must be a non-empty string")),
354
363
  skills: optional(z3.array(z3.string())),
@@ -728,6 +737,7 @@ import { join as join5 } from "path";
728
737
 
729
738
  // src/utils/frontmatter.ts
730
739
  import matter from "gray-matter";
740
+ import { dump, load } from "js-yaml";
731
741
  function isPlainObject(value) {
732
742
  if (value === null || typeof value !== "object") return false;
733
743
  const prototype = Object.getPrototypeOf(value);
@@ -766,8 +776,55 @@ function deepRemoveNullishObject(obj) {
766
776
  }
767
777
  return result;
768
778
  }
769
- function stringifyFrontmatter(body, frontmatter) {
770
- const cleanFrontmatter = deepRemoveNullishObject(frontmatter);
779
+ function deepFlattenStringsValue(value) {
780
+ if (value === null || value === void 0) {
781
+ return void 0;
782
+ }
783
+ if (typeof value === "string") {
784
+ return value.replace(/\n+/g, " ").trim();
785
+ }
786
+ if (Array.isArray(value)) {
787
+ const cleanedArray = value.map((item) => deepFlattenStringsValue(item)).filter((item) => item !== void 0);
788
+ return cleanedArray;
789
+ }
790
+ if (isPlainObject(value)) {
791
+ const result = {};
792
+ for (const [key, val] of Object.entries(value)) {
793
+ const cleaned = deepFlattenStringsValue(val);
794
+ if (cleaned !== void 0) {
795
+ result[key] = cleaned;
796
+ }
797
+ }
798
+ return result;
799
+ }
800
+ return value;
801
+ }
802
+ function deepFlattenStringsObject(obj) {
803
+ if (!obj || typeof obj !== "object") {
804
+ return {};
805
+ }
806
+ const result = {};
807
+ for (const [key, val] of Object.entries(obj)) {
808
+ const cleaned = deepFlattenStringsValue(val);
809
+ if (cleaned !== void 0) {
810
+ result[key] = cleaned;
811
+ }
812
+ }
813
+ return result;
814
+ }
815
+ function stringifyFrontmatter(body, frontmatter, options) {
816
+ const { avoidBlockScalars = false } = options ?? {};
817
+ const cleanFrontmatter = avoidBlockScalars ? deepFlattenStringsObject(frontmatter) : deepRemoveNullishObject(frontmatter);
818
+ if (avoidBlockScalars) {
819
+ return matter.stringify(body, cleanFrontmatter, {
820
+ engines: {
821
+ yaml: {
822
+ parse: (input) => load(input) ?? {},
823
+ stringify: (data) => dump(data, { lineWidth: -1 })
824
+ }
825
+ }
826
+ });
827
+ }
771
828
  return matter.stringify(body, cleanFrontmatter);
772
829
  }
773
830
  function parseFrontmatter(content, filePath) {
@@ -1886,7 +1943,7 @@ var CursorCommand = class _CursorCommand extends ToolCommand {
1886
1943
  }
1887
1944
  super({
1888
1945
  ...rest,
1889
- fileContent: stringifyFrontmatter(body, frontmatter)
1946
+ fileContent: stringifyFrontmatter(body, frontmatter, { avoidBlockScalars: true })
1890
1947
  });
1891
1948
  this.frontmatter = frontmatter;
1892
1949
  this.body = body;
@@ -6312,12 +6369,26 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6312
6369
  json;
6313
6370
  constructor(params) {
6314
6371
  super(params);
6315
- this.json = this.fileContent !== void 0 ? JSON.parse(this.fileContent) : {};
6372
+ if (this.fileContent !== void 0) {
6373
+ try {
6374
+ this.json = JSON.parse(this.fileContent);
6375
+ } catch (error) {
6376
+ throw new Error(
6377
+ `Failed to parse Cursor MCP config at ${join46(this.relativeDirPath, this.relativeFilePath)}: ${formatError(error)}`,
6378
+ { cause: error }
6379
+ );
6380
+ }
6381
+ } else {
6382
+ this.json = {};
6383
+ }
6316
6384
  }
6317
6385
  getJson() {
6318
6386
  return this.json;
6319
6387
  }
6320
- static getSettablePaths() {
6388
+ isDeletable() {
6389
+ return !this.global;
6390
+ }
6391
+ static getSettablePaths(_options) {
6321
6392
  return {
6322
6393
  relativeDirPath: ".cursor",
6323
6394
  relativeFilePath: "mcp.json"
@@ -6325,41 +6396,62 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6325
6396
  }
6326
6397
  static async fromFile({
6327
6398
  baseDir = process.cwd(),
6328
- validate = true
6399
+ validate = true,
6400
+ global = false
6329
6401
  }) {
6330
- const fileContent = await readFileContent(
6331
- join46(
6332
- baseDir,
6333
- this.getSettablePaths().relativeDirPath,
6334
- this.getSettablePaths().relativeFilePath
6335
- )
6336
- );
6402
+ const paths = this.getSettablePaths({ global });
6403
+ const filePath = join46(baseDir, paths.relativeDirPath, paths.relativeFilePath);
6404
+ const fileContent = await readFileContentOrNull(filePath) ?? '{"mcpServers":{}}';
6405
+ let json;
6406
+ try {
6407
+ json = JSON.parse(fileContent);
6408
+ } catch (error) {
6409
+ throw new Error(
6410
+ `Failed to parse Cursor MCP config at ${join46(paths.relativeDirPath, paths.relativeFilePath)}: ${formatError(error)}`,
6411
+ { cause: error }
6412
+ );
6413
+ }
6414
+ const newJson = { ...json, mcpServers: json.mcpServers ?? {} };
6337
6415
  return new _CursorMcp({
6338
6416
  baseDir,
6339
- relativeDirPath: this.getSettablePaths().relativeDirPath,
6340
- relativeFilePath: this.getSettablePaths().relativeFilePath,
6341
- fileContent,
6342
- validate
6417
+ relativeDirPath: paths.relativeDirPath,
6418
+ relativeFilePath: paths.relativeFilePath,
6419
+ fileContent: JSON.stringify(newJson, null, 2),
6420
+ validate,
6421
+ global
6343
6422
  });
6344
6423
  }
6345
- static fromRulesyncMcp({
6424
+ static async fromRulesyncMcp({
6346
6425
  baseDir = process.cwd(),
6347
6426
  rulesyncMcp,
6348
- validate = true
6427
+ validate = true,
6428
+ global = false
6349
6429
  }) {
6350
- const json = rulesyncMcp.getJson();
6351
- const mcpServers = isMcpServers(json.mcpServers) ? json.mcpServers : {};
6430
+ const paths = this.getSettablePaths({ global });
6431
+ const fileContent = await readOrInitializeFileContent(
6432
+ join46(baseDir, paths.relativeDirPath, paths.relativeFilePath),
6433
+ JSON.stringify({ mcpServers: {} }, null, 2)
6434
+ );
6435
+ let json;
6436
+ try {
6437
+ json = JSON.parse(fileContent);
6438
+ } catch (error) {
6439
+ throw new Error(
6440
+ `Failed to parse Cursor MCP config at ${join46(paths.relativeDirPath, paths.relativeFilePath)}: ${formatError(error)}`,
6441
+ { cause: error }
6442
+ );
6443
+ }
6444
+ const rulesyncJson = rulesyncMcp.getJson();
6445
+ const mcpServers = isMcpServers(rulesyncJson.mcpServers) ? rulesyncJson.mcpServers : {};
6352
6446
  const transformedServers = convertEnvToCursorFormat(mcpServers);
6353
- const cursorConfig = {
6354
- mcpServers: transformedServers
6355
- };
6356
- const fileContent = JSON.stringify(cursorConfig, null, 2);
6447
+ const cursorConfig = { ...json, mcpServers: transformedServers };
6357
6448
  return new _CursorMcp({
6358
6449
  baseDir,
6359
- relativeDirPath: this.getSettablePaths().relativeDirPath,
6360
- relativeFilePath: this.getSettablePaths().relativeFilePath,
6361
- fileContent,
6362
- validate
6450
+ relativeDirPath: paths.relativeDirPath,
6451
+ relativeFilePath: paths.relativeFilePath,
6452
+ fileContent: JSON.stringify(cursorConfig, null, 2),
6453
+ validate,
6454
+ global
6363
6455
  });
6364
6456
  }
6365
6457
  toRulesyncMcp() {
@@ -6383,14 +6475,16 @@ var CursorMcp = class _CursorMcp extends ToolMcp {
6383
6475
  static forDeletion({
6384
6476
  baseDir = process.cwd(),
6385
6477
  relativeDirPath,
6386
- relativeFilePath
6478
+ relativeFilePath,
6479
+ global = false
6387
6480
  }) {
6388
6481
  return new _CursorMcp({
6389
6482
  baseDir,
6390
6483
  relativeDirPath,
6391
6484
  relativeFilePath,
6392
6485
  fileContent: "{}",
6393
- validate: false
6486
+ validate: false,
6487
+ global
6394
6488
  });
6395
6489
  }
6396
6490
  };
@@ -7219,7 +7313,7 @@ var toolMcpFactories = /* @__PURE__ */ new Map([
7219
7313
  class: CursorMcp,
7220
7314
  meta: {
7221
7315
  supportsProject: true,
7222
- supportsGlobal: false,
7316
+ supportsGlobal: true,
7223
7317
  supportsEnabledTools: false,
7224
7318
  supportsDisabledTools: false
7225
7319
  }
@@ -7932,9 +8026,15 @@ import { join as join59 } from "path";
7932
8026
  var DirFeatureProcessor = class {
7933
8027
  baseDir;
7934
8028
  dryRun;
7935
- constructor({ baseDir = process.cwd(), dryRun = false }) {
8029
+ avoidBlockScalars;
8030
+ constructor({
8031
+ baseDir = process.cwd(),
8032
+ dryRun = false,
8033
+ avoidBlockScalars = false
8034
+ }) {
7936
8035
  this.baseDir = baseDir;
7937
8036
  this.dryRun = dryRun;
8037
+ this.avoidBlockScalars = avoidBlockScalars;
7938
8038
  }
7939
8039
  /**
7940
8040
  * Return tool targets that this feature supports.
@@ -7960,7 +8060,9 @@ var DirFeatureProcessor = class {
7960
8060
  let mainFileContent;
7961
8061
  if (mainFile) {
7962
8062
  const mainFilePath = join59(dirPath, mainFile.name);
7963
- const content = stringifyFrontmatter(mainFile.body, mainFile.frontmatter);
8063
+ const content = stringifyFrontmatter(mainFile.body, mainFile.frontmatter, {
8064
+ avoidBlockScalars: this.avoidBlockScalars
8065
+ });
7964
8066
  mainFileContent = addTrailingNewline(content);
7965
8067
  const existingContent = await readFileContentOrNull(mainFilePath);
7966
8068
  if (existingContent !== mainFileContent) {
@@ -10699,7 +10801,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
10699
10801
  getFactory = defaultGetFactory4,
10700
10802
  dryRun = false
10701
10803
  }) {
10702
- super({ baseDir, dryRun });
10804
+ super({ baseDir, dryRun, avoidBlockScalars: toolTarget === "cursor" });
10703
10805
  const result = SkillsProcessorToolTargetSchema.safeParse(toolTarget);
10704
10806
  if (!result.success) {
10705
10807
  throw new Error(
@@ -11780,7 +11882,7 @@ var CursorSubagent = class _CursorSubagent extends ToolSubagent {
11780
11882
  ...cursorSection
11781
11883
  };
11782
11884
  const body = rulesyncSubagent.getBody();
11783
- const fileContent = stringifyFrontmatter(body, cursorFrontmatter);
11885
+ const fileContent = stringifyFrontmatter(body, cursorFrontmatter, { avoidBlockScalars: true });
11784
11886
  const paths = this.getSettablePaths({ global });
11785
11887
  return new _CursorSubagent({
11786
11888
  baseDir,
@@ -16915,6 +17017,7 @@ export {
16915
17017
  ALL_FEATURES,
16916
17018
  ALL_FEATURES_WITH_WILDCARD,
16917
17019
  ALL_TOOL_TARGETS,
17020
+ findControlCharacter,
16918
17021
  ConfigResolver,
16919
17022
  stringifyFrontmatter,
16920
17023
  RulesyncCommandFrontmatterSchema,