rulesync 16.0.0 → 16.2.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
@@ -96,8 +96,8 @@ The tables below show whether each tool supports a given feature (✅ = supporte
96
96
  | GitHub Copilot CLI | ✅ | | ✅ | | ✅ | ✅ | ✅ | | |
97
97
  | Goose | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
98
98
  | Hermes Agent | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
99
- | Grok CLI | ✅ | | ✅ | | ✅ | ✅ | ✅ | ✅ | |
100
- | Cursor | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
99
+ | Grok CLI | ✅ | | ✅ || ✅ | ✅ | ✅ | ✅ | |
100
+ | Cursor | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ ||
101
101
  | deepagents-cli | ✅ | | ✅ | | ✅ | ✅ | ✅ | | |
102
102
  | Factory Droid | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
103
103
  | OpenCode | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
@@ -105,17 +105,17 @@ The tables below show whether each tool supports a given feature (✅ = supporte
105
105
  | Kilo Code | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
106
106
  | Kimi Code | ✅ | | ✅ | | ✅ | ✅ | ✅ | ✅ | |
107
107
  | Roo Code | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | |
108
- | Rovodev (Atlassian) | ✅ | | ✅ | ✅ | ✅ | ✅ | | ✅ | |
108
+ | Rovodev (Atlassian) | ✅ | | ✅ | ✅ | ✅ | ✅ | | ✅ ||
109
109
  | Takt | ✅ | | ✅ | ✅ | ✅ | ✅ | | ✅ | ✅ |
110
110
  | Vibe Code | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | ✅ | |
111
111
  | Qwen Code | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
112
- | Reasonix | ✅ | | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
112
+ | Reasonix | ✅ || ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
113
113
  | Kiro ⚠️ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
114
114
  | Kiro CLI | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
115
115
  | Kiro IDE | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
116
- | Google Antigravity IDE | ✅ | | ✅ | ✅ | | ✅ | ✅ | ✅ | |
117
- | Google Antigravity CLI | ✅ | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | |
118
- | Google Antigravity plugin | ✅ | | ✅ | | | ✅ | ✅ | | |
116
+ | Google Antigravity IDE | ✅ | | ✅ | ✅ || ✅ | ✅ | ✅ | |
117
+ | Google Antigravity CLI | ✅ | ✅ | ✅ | ✅ || ✅ | ✅ | ✅ | |
118
+ | Google Antigravity plugin | ✅ | | ✅ | || ✅ | ✅ | | |
119
119
  | JetBrains AI Assistant | ✅ | ✅ | ✅ | | | ✅ | | | |
120
120
  | JetBrains Junie | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
121
121
  | AugmentCode | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- const require_import = require("../import-Dyo6JaHg.cjs");
2
+ const require_import = require("../import-DNfxS6QZ.cjs");
3
3
  let commander = require("commander");
4
4
  let zod_mini = require("zod/mini");
5
5
  let node_fs_promises = require("node:fs/promises");
@@ -18,9 +18,9 @@ let node_util = require("node:util");
18
18
  let _octokit_request_error = require("@octokit/request-error");
19
19
  let _octokit_rest = require("@octokit/rest");
20
20
  let node_zlib = require("node:zlib");
21
- let fastmcp = require("fastmcp");
22
21
  let node_fs = require("node:fs");
23
22
  node_fs = require_import.__toESM(node_fs, 1);
23
+ let fastmcp = require("fastmcp");
24
24
  let node_stream = require("node:stream");
25
25
  let node_stream_promises = require("node:stream/promises");
26
26
  //#region src/utils/parse-comma-separated-list.ts
@@ -4376,6 +4376,216 @@ async function fetchCommand(logger, options) {
4376
4376
  throw error;
4377
4377
  }
4378
4378
  }
4379
+ /**
4380
+ * Coalesces file-system change notifications into debounced, non-overlapping
4381
+ * runs.
4382
+ *
4383
+ * Guarantees:
4384
+ * - At most one `run` is in flight at any time.
4385
+ * - Every notified path is reported to exactly one `run` as a trigger.
4386
+ * - Notifications that arrive while a run is in flight schedule exactly one
4387
+ * follow-up run after it finishes, so a change is never lost and never
4388
+ * causes a run per event.
4389
+ */
4390
+ var WatchScheduler = class {
4391
+ run;
4392
+ onError;
4393
+ debounceMs;
4394
+ pending = /* @__PURE__ */ new Set();
4395
+ timer;
4396
+ running;
4397
+ closed = false;
4398
+ constructor({ run, onError, debounceMs = 300 }) {
4399
+ this.run = run;
4400
+ this.onError = onError;
4401
+ this.debounceMs = debounceMs;
4402
+ }
4403
+ notify({ path }) {
4404
+ if (this.closed) return;
4405
+ this.pending.add(path);
4406
+ this.schedule();
4407
+ }
4408
+ /**
4409
+ * Stops accepting notifications and waits for an in-flight run to settle.
4410
+ * Pending (not yet started) changes are dropped.
4411
+ */
4412
+ async close() {
4413
+ this.closed = true;
4414
+ this.clearTimer();
4415
+ this.pending.clear();
4416
+ await this.running;
4417
+ }
4418
+ clearTimer() {
4419
+ if (this.timer !== void 0) {
4420
+ clearTimeout(this.timer);
4421
+ this.timer = void 0;
4422
+ }
4423
+ }
4424
+ schedule() {
4425
+ this.clearTimer();
4426
+ this.timer = setTimeout(() => {
4427
+ this.timer = void 0;
4428
+ this.flush();
4429
+ }, this.debounceMs);
4430
+ }
4431
+ async flush() {
4432
+ if (this.closed || this.running !== void 0 || this.pending.size === 0) return;
4433
+ const triggers = [...this.pending];
4434
+ this.pending.clear();
4435
+ const running = (async () => {
4436
+ try {
4437
+ await this.run({ triggers });
4438
+ } catch (error) {
4439
+ this.onError({
4440
+ error,
4441
+ triggers
4442
+ });
4443
+ }
4444
+ })();
4445
+ this.running = running;
4446
+ await running;
4447
+ this.running = void 0;
4448
+ if (!this.closed && this.pending.size > 0) this.schedule();
4449
+ }
4450
+ };
4451
+ /**
4452
+ * Watches one directory, re-attaching the underlying `fs.watch` if the
4453
+ * directory is deleted and later recreated.
4454
+ *
4455
+ * Without this, a `git checkout` to a branch without `.rulesync/` (or any
4456
+ * tool that replaces the directory rather than its contents) would silently
4457
+ * kill the watcher: the deleted inode emits no further events and no error,
4458
+ * so watch mode would keep running while never regenerating again.
4459
+ *
4460
+ * The first attach is not guarded — a missing directory at startup is a real
4461
+ * configuration error and must surface to the caller.
4462
+ */
4463
+ function watchTargetWithRearm({ target, onChange, onError, rearmIntervalMs }) {
4464
+ let watcher;
4465
+ let rearmTimer;
4466
+ let closed = false;
4467
+ const attach = () => {
4468
+ const created = (0, node_fs.watch)(target.directory, {
4469
+ recursive: target.recursive,
4470
+ persistent: true
4471
+ }, (_eventType, filename) => {
4472
+ if (filename === null || filename === void 0) {
4473
+ onChange({ path: target.directory });
4474
+ verifyStillWatching();
4475
+ return;
4476
+ }
4477
+ const relativePath = filename.toString();
4478
+ if (target.include && !target.include(relativePath)) {
4479
+ verifyStillWatching();
4480
+ return;
4481
+ }
4482
+ onChange({ path: (0, node_path.join)(target.directory, relativePath) });
4483
+ verifyStillWatching();
4484
+ });
4485
+ created.on("error", (error) => {
4486
+ onError({
4487
+ error,
4488
+ directory: target.directory
4489
+ });
4490
+ verifyStillWatching();
4491
+ });
4492
+ watcher = created;
4493
+ };
4494
+ const scheduleRearm = () => {
4495
+ if (closed || rearmTimer !== void 0) return;
4496
+ rearmTimer = setInterval(() => {
4497
+ if (closed || !(0, node_fs.existsSync)(target.directory)) return;
4498
+ clearInterval(rearmTimer);
4499
+ rearmTimer = void 0;
4500
+ try {
4501
+ attach();
4502
+ } catch (error) {
4503
+ onError({
4504
+ error,
4505
+ directory: target.directory
4506
+ });
4507
+ scheduleRearm();
4508
+ return;
4509
+ }
4510
+ onChange({ path: target.directory });
4511
+ }, rearmIntervalMs);
4512
+ };
4513
+ const verifyStillWatching = () => {
4514
+ if (closed || watcher === void 0 || (0, node_fs.existsSync)(target.directory)) return;
4515
+ watcher.close();
4516
+ watcher = void 0;
4517
+ scheduleRearm();
4518
+ };
4519
+ attach();
4520
+ return { close: () => {
4521
+ closed = true;
4522
+ if (rearmTimer !== void 0) {
4523
+ clearInterval(rearmTimer);
4524
+ rearmTimer = void 0;
4525
+ }
4526
+ watcher?.close();
4527
+ watcher = void 0;
4528
+ } };
4529
+ }
4530
+ /**
4531
+ * Starts one watcher per target and forwards matching events to `onChange` as
4532
+ * absolute paths. If any target fails to attach, the watchers started so far
4533
+ * are closed before the error propagates, so no descriptor is leaked.
4534
+ */
4535
+ function watchTargets({ targets, onChange, onError, rearmIntervalMs = 500 }) {
4536
+ const handles = [];
4537
+ const closeAll = () => {
4538
+ for (const handle of handles) handle.close();
4539
+ };
4540
+ try {
4541
+ for (const target of targets) handles.push(watchTargetWithRearm({
4542
+ target,
4543
+ onChange,
4544
+ onError,
4545
+ rearmIntervalMs
4546
+ }));
4547
+ } catch (error) {
4548
+ closeAll();
4549
+ throw error;
4550
+ }
4551
+ return { close: closeAll };
4552
+ }
4553
+ /**
4554
+ * Builds the set of directories watch mode observes: the `.rulesync/` source
4555
+ * tree (recursively) and, filtered down to the configuration files themselves,
4556
+ * the directory holding `rulesync.jsonc`.
4557
+ *
4558
+ * Only input paths are watched. Generated output lives outside `.rulesync/`, so
4559
+ * a regeneration cannot re-trigger the watcher.
4560
+ */
4561
+ function buildWatchTargets({ inputRoot, configFilePath }) {
4562
+ const configFilePaths = buildConfigFilePaths({ configFilePath });
4563
+ return [{
4564
+ directory: (0, node_path.join)(inputRoot, require_import.RULESYNC_RELATIVE_DIR_PATH),
4565
+ recursive: true
4566
+ }, {
4567
+ directory: (0, node_path.dirname)(configFilePath),
4568
+ recursive: false,
4569
+ include: (relativePath) => configFilePaths.has((0, node_path.join)((0, node_path.dirname)(configFilePath), relativePath))
4570
+ }];
4571
+ }
4572
+ /**
4573
+ * The absolute paths of the configuration files watch mode observes: the base
4574
+ * configuration file and the `rulesync.local.jsonc` sitting next to it, which
4575
+ * is exactly what `ConfigResolver` loads.
4576
+ */
4577
+ function buildConfigFilePaths({ configFilePath }) {
4578
+ return /* @__PURE__ */ new Set([configFilePath, (0, node_path.join)((0, node_path.dirname)(configFilePath), require_import.RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH)]);
4579
+ }
4580
+ /**
4581
+ * Renders trigger paths relative to `baseDir` for logging, truncating long
4582
+ * bursts so a `git checkout` does not flood the terminal.
4583
+ */
4584
+ function formatTriggerPaths({ triggers, baseDir, max = 5 }) {
4585
+ const displayed = triggers.slice(0, max).map((trigger) => (0, node_path.relative)(baseDir, trigger) || trigger);
4586
+ const remaining = triggers.length - displayed.length;
4587
+ return remaining > 0 ? `${displayed.join(", ")} (+${remaining} more)` : displayed.join(", ");
4588
+ }
4379
4589
  //#endregion
4380
4590
  //#region src/cli/commands/generate.ts
4381
4591
  /**
@@ -4465,7 +4675,20 @@ function buildSummaryParts(result) {
4465
4675
  return parts;
4466
4676
  }
4467
4677
  async function generateCommand(logger, options) {
4468
- const config = await require_import.ConfigResolver.resolve(options, { logger });
4678
+ if (options.watch) {
4679
+ await generateWatchCommand(logger, options);
4680
+ return;
4681
+ }
4682
+ await generateOnce(logger, options);
4683
+ }
4684
+ /**
4685
+ * Runs one generation. `resolvedConfig` lets a caller that already resolved
4686
+ * the configuration (watch mode's startup validation) reuse it instead of
4687
+ * paying for a second resolution — and, more importantly, instead of emitting
4688
+ * the resolver's warnings twice.
4689
+ */
4690
+ async function generateOnce(logger, options, { resolvedConfig } = {}) {
4691
+ const config = resolvedConfig ?? await require_import.ConfigResolver.resolve(options, { logger });
4469
4692
  const check = config.getCheck();
4470
4693
  const isPreview = config.isPreviewMode();
4471
4694
  const modePrefix = isPreview ? "[DRY RUN]" : "";
@@ -4560,6 +4783,76 @@ async function generateCommand(logger, options) {
4560
4783
  if (isPreview) logger.info(`${modePrefix} Would write ${totalGenerated} file(s) total (${parts.join(" + ")})`);
4561
4784
  else logger.success(`🎉 All done! Written ${totalGenerated} file(s) total (${parts.join(" + ")})`);
4562
4785
  }
4786
+ /**
4787
+ * Rejects flag combinations that contradict a long-running watch: `--check`
4788
+ * and `--dry-run` are one-shot verification modes (the former is meant to exit
4789
+ * non-zero), and `--json` buffers a single result document until the command
4790
+ * returns, which never happens while watching.
4791
+ */
4792
+ function assertWatchModeCompatible({ isCheck, isDryRun, isJsonMode }) {
4793
+ const conflicts = [
4794
+ isCheck ? "--check" : void 0,
4795
+ isDryRun ? "--dry-run" : void 0,
4796
+ isJsonMode ? "--json" : void 0
4797
+ ].filter((flag) => flag !== void 0);
4798
+ if (conflicts.length > 0) throw new require_import.CLIError(`--watch cannot be combined with ${conflicts.join(", ")}.`, require_import.ErrorCodes.VALIDATION_FAILED);
4799
+ }
4800
+ async function generateWatchCommand(logger, options) {
4801
+ const config = await require_import.ConfigResolver.resolve(options, { logger });
4802
+ assertWatchModeCompatible({
4803
+ isCheck: config.getCheck(),
4804
+ isDryRun: config.getDryRun(),
4805
+ isJsonMode: logger.jsonMode
4806
+ });
4807
+ const inputRoot = config.getInputRoot();
4808
+ const configFilePath = config.getConfigFilePath();
4809
+ const configFilePaths = buildConfigFilePaths({ configFilePath });
4810
+ await generateOnce(logger, options, { resolvedConfig: config });
4811
+ const targets = buildWatchTargets({
4812
+ inputRoot,
4813
+ configFilePath
4814
+ });
4815
+ const scheduler = new WatchScheduler({
4816
+ run: async ({ triggers }) => {
4817
+ logger.info(`\nChange detected: ${formatTriggerPaths({
4818
+ triggers,
4819
+ baseDir: inputRoot
4820
+ })}`);
4821
+ if (triggers.some((trigger) => configFilePaths.has(trigger))) logger.warn("Configuration file changed. The set of watched paths is fixed at startup — restart 'rulesync generate --watch' if you changed 'inputRoot' or the configuration file location.");
4822
+ await generateOnce(logger, options);
4823
+ },
4824
+ onError: ({ error }) => {
4825
+ logger.error(`Generation failed: ${require_import.formatError(error)}`);
4826
+ logger.info("Still watching for changes...");
4827
+ }
4828
+ });
4829
+ const handle = watchTargets({
4830
+ targets,
4831
+ onChange: ({ path }) => {
4832
+ scheduler.notify({ path });
4833
+ },
4834
+ onError: ({ error, directory }) => {
4835
+ logger.error(`Watch error on ${directory}: ${require_import.formatError(error)}`);
4836
+ }
4837
+ });
4838
+ logger.info(`\nWatching for changes in:\n${targets.map((target) => ` ${target.directory}`).join("\n")}`);
4839
+ logger.info("Press Ctrl+C to stop.");
4840
+ await new Promise((resolveShutdown) => {
4841
+ const shutdown = () => {
4842
+ process.off("SIGINT", shutdown);
4843
+ process.off("SIGTERM", shutdown);
4844
+ handle.close();
4845
+ scheduler.close().catch((error) => {
4846
+ logger.error(`Failed to stop the watcher cleanly: ${require_import.formatError(error)}`);
4847
+ }).finally(() => {
4848
+ logger.info("\nStopped watching.");
4849
+ resolveShutdown();
4850
+ });
4851
+ };
4852
+ process.once("SIGINT", shutdown);
4853
+ process.once("SIGTERM", shutdown);
4854
+ });
4855
+ }
4563
4856
  //#endregion
4564
4857
  //#region src/cli/commands/gitignore-derive.ts
4565
4858
  const TARGETS_NOT_DERIVED = /* @__PURE__ */ new Set([
@@ -9004,7 +9297,7 @@ function wrapCommand$1({ name, errorCode, handler, getVersion, loggerFactory = c
9004
9297
  }
9005
9298
  //#endregion
9006
9299
  //#region src/cli/program.ts
9007
- const getVersion = () => "16.0.0";
9300
+ const getVersion = () => "16.2.0";
9008
9301
  const FEATURES_HELP = `${require_import.ALL_FEATURES.join(",")}; ignore is deprecated, use permissions`;
9009
9302
  function wrapCommand(name, errorCode, handler) {
9010
9303
  return wrapCommand$1({
@@ -9073,7 +9366,7 @@ function createProgram() {
9073
9366
  silent: options.silent
9074
9367
  });
9075
9368
  }));
9076
- 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) => {
9369
+ 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)").option("-w, --watch", "Keep running and regenerate whenever rulesync source files change (cannot be combined with --check, --dry-run or --json)").action(wrapCommand("generate", "GENERATION_FAILED", async (logger, options) => {
9077
9370
  await generateCommand(logger, options);
9078
9371
  }));
9079
9372
  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) => {