rulesync 16.36.0 → 16.37.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.
@@ -8445,6 +8445,7 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
8445
8445
  trigger: zod_mini.z.optional(zod_mini.z.string()),
8446
8446
  globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
8447
8447
  })),
8448
+ aiassistant: zod_mini.z.optional(zod_mini.z.looseObject({ apply: zod_mini.z.optional(zod_mini.z.string()) })),
8448
8449
  devin: zod_mini.z.optional(zod_mini.z.looseObject({
8449
8450
  trigger: zod_mini.z.optional(zod_mini.z.string()),
8450
8451
  globs: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
@@ -9669,6 +9670,8 @@ const RulesyncSkillFrontmatterSchema = zod_mini.z.looseObject({
9669
9670
  paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
9670
9671
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
9671
9672
  "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
9673
+ icon: zod_mini.z.optional(zod_mini.z.string()),
9674
+ color: zod_mini.z.optional(zod_mini.z.string()),
9672
9675
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
9673
9676
  })),
9674
9677
  factorydroid: zod_mini.z.optional(zod_mini.z.looseObject({
@@ -23683,14 +23686,23 @@ var ContinueHooks = class extends SettingsJsonHooks {
23683
23686
  * portable `command` field is written, which upstream copies to both when
23684
23687
  * neither is present. Note the cloud agent runs hooks in a Linux sandbox and
23685
23688
  * honors only `bash` and `command` — a `powershell` entry is ignored there.
23689
+ * VS Code reads the same file and maps `bash` to `osx` / `linux` and
23690
+ * `powershell` to `windows`; it also documents its own per-OS overrides
23691
+ * (`windows`, `linux`, `osx`) next to `command`, which are typed here so an
23692
+ * entry that uses them instead of a shell field is not mistaken for one
23693
+ * without a command.
23686
23694
  *
23687
23695
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
23696
+ * @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
23688
23697
  */
23689
23698
  const CopilotHookEntrySchema = zod_mini.z.looseObject({
23690
23699
  type: zod_mini.z.string(),
23691
23700
  bash: zod_mini.z.optional(zod_mini.z.string()),
23692
23701
  powershell: zod_mini.z.optional(zod_mini.z.string()),
23693
23702
  command: zod_mini.z.optional(zod_mini.z.string()),
23703
+ windows: zod_mini.z.optional(zod_mini.z.string()),
23704
+ linux: zod_mini.z.optional(zod_mini.z.string()),
23705
+ osx: zod_mini.z.optional(zod_mini.z.string()),
23694
23706
  cwd: zod_mini.z.optional(zod_mini.z.string()),
23695
23707
  env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
23696
23708
  timeoutSec: zod_mini.z.optional(zod_mini.z.number()),
@@ -23781,17 +23793,30 @@ function resolveImportCommand$1(entry, logger) {
23781
23793
  *
23782
23794
  * Generate re-emits any non-canonical key verbatim through `rest`, so a key
23783
23795
  * dropped here does not survive an import → generate round trip. `cwd` is a
23784
- * documented Copilot hook field and was previously lost that way.
23796
+ * documented Copilot hook field and was previously lost that way, as were the
23797
+ * VS Code per-OS overrides `windows` / `linux` / `osx`: an entry authored with
23798
+ * only those (no `command`) imported as a hook with no command at all and was
23799
+ * regenerated empty.
23785
23800
  *
23786
23801
  * @see https://docs.github.com/en/copilot/reference/hooks-reference
23787
23802
  * @see https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks
23803
+ * @see https://code.visualstudio.com/docs/agents/reference/hooks-reference
23788
23804
  */
23789
23805
  function importPassthrough$1(entry) {
23790
23806
  const passthrough = {};
23791
23807
  if (entry.cwd !== void 0) passthrough.cwd = entry.cwd;
23792
23808
  if (entry.env !== void 0) passthrough.env = entry.env;
23809
+ for (const key of OS_OVERRIDE_KEYS) if (entry[key] !== void 0) passthrough[key] = entry[key];
23793
23810
  return passthrough;
23794
23811
  }
23812
+ const OS_OVERRIDE_KEYS = [
23813
+ "windows",
23814
+ "linux",
23815
+ "osx"
23816
+ ];
23817
+ function hasOsOverride(passthrough) {
23818
+ return OS_OVERRIDE_KEYS.some((key) => passthrough[key] !== void 0);
23819
+ }
23795
23820
  /**
23796
23821
  * Extract hooks from Copilot hooks JSON into canonical format.
23797
23822
  * Copilot format: { version: 1, hooks: { eventName: [...hookEntries] } }
@@ -23812,12 +23837,14 @@ function copilotHooksToCanonical(copilotHooks, logger) {
23812
23837
  const entry = parseResult.data;
23813
23838
  const { command, shell } = resolveImportCommand$1(entry, logger);
23814
23839
  const timeout = entry.timeoutSec ?? entry.timeout;
23840
+ const passthrough = importPassthrough$1(entry);
23841
+ if (command === void 0 && hasOsOverride(passthrough)) logger?.warn(`Copilot hook on '${copilotEventName}' has only VS Code per-OS overrides (windows/linux/osx) and no portable command; it is preserved for copilot, but other targets will generate it without a command until a \`command\` is added.`);
23815
23842
  defs.push({
23816
23843
  type: "command",
23817
23844
  ...command !== void 0 && { command },
23818
23845
  ...shell !== void 0 && { shell },
23819
23846
  ...timeout !== void 0 && { timeout },
23820
- ...importPassthrough$1(entry)
23847
+ ...passthrough
23821
23848
  });
23822
23849
  }
23823
23850
  if (defs.length > 0) canonical[eventName] = defs;
@@ -23980,6 +24007,8 @@ const CopilotCliHookEntrySchema = zod_mini.z.looseObject({
23980
24007
  bash: zod_mini.z.optional(zod_mini.z.string()),
23981
24008
  powershell: zod_mini.z.optional(zod_mini.z.string()),
23982
24009
  command: zod_mini.z.optional(zod_mini.z.string()),
24010
+ exec: zod_mini.z.optional(zod_mini.z.string()),
24011
+ args: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
23983
24012
  prompt: zod_mini.z.optional(zod_mini.z.string()),
23984
24013
  url: zod_mini.z.optional(zod_mini.z.string()),
23985
24014
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
@@ -24009,6 +24038,32 @@ function resolveExportMatcherPart({ matcher, matcherSupported, eventName, logger
24009
24038
  return {};
24010
24039
  }
24011
24040
  /**
24041
+ * Pick the command fields of an exported `command` entry.
24042
+ *
24043
+ * A canonical `args` list (an empty one counts) selects the CLI's shell-free
24044
+ * form: `command` becomes `exec` and the list is written as `args`, so the
24045
+ * executable is spawned directly. The `shell` selector is meaningless there
24046
+ * and is dropped with a warning. Otherwise the canonical `shell` selector
24047
+ * picks the shell-specific field; without it the portable `command` field is
24048
+ * written, which upstream copies to both `bash` and `powershell`. Keying off
24049
+ * `shell` rather than `process.platform` keeps the generated file identical
24050
+ * on every machine.
24051
+ */
24052
+ function resolveExportCommandPart({ def, eventName, logger }) {
24053
+ if (Array.isArray(def.args)) {
24054
+ if (def.shell !== void 0) logger?.warn(`Copilot CLI runs an \`exec\` hook without a shell; dropping shell "${def.shell}" on '${eventName}'.`);
24055
+ return compact({
24056
+ exec: def.command,
24057
+ args: def.args,
24058
+ env: def.env
24059
+ });
24060
+ }
24061
+ return compact({
24062
+ [def.shell ?? "command"]: def.command,
24063
+ env: def.env
24064
+ });
24065
+ }
24066
+ /**
24012
24067
  * Build the exported entries for a single canonical event. Returns an empty
24013
24068
  * array when no entries are emitted (e.g. all prompt hooks were skipped).
24014
24069
  */
@@ -24048,19 +24103,17 @@ function buildCopilotCliEntriesForEvent({ eventName, definitions, canonicalSchem
24048
24103
  ...timeoutPart,
24049
24104
  ...rest
24050
24105
  });
24051
- else if (hookType === "command") {
24052
- const commandField = def.shell ?? "command";
24053
- entries.push({
24054
- type: "command",
24055
- ...matcherPart,
24056
- ...compact({
24057
- [commandField]: def.command,
24058
- env: def.env
24059
- }),
24060
- ...timeoutPart,
24061
- ...rest
24062
- });
24063
- }
24106
+ else if (hookType === "command") entries.push({
24107
+ type: "command",
24108
+ ...matcherPart,
24109
+ ...resolveExportCommandPart({
24110
+ def,
24111
+ eventName,
24112
+ logger
24113
+ }),
24114
+ ...timeoutPart,
24115
+ ...rest
24116
+ });
24064
24117
  }
24065
24118
  return entries;
24066
24119
  }
@@ -24100,6 +24153,10 @@ function importPassthrough(entry) {
24100
24153
  /**
24101
24154
  * Resolve the canonical command and its `shell` selector from an imported entry.
24102
24155
  *
24156
+ * The CLI-only `exec` form maps to canonical `command` + `args`, the exec form
24157
+ * shared with AugmentCode and Claude Code, so it no longer imports as a hook
24158
+ * with no command at all.
24159
+ *
24103
24160
  * A shell-specific field carries its `shell` through so re-export writes the
24104
24161
  * same field back. An entry using only the portable `command` field leaves
24105
24162
  * `shell` unset, which re-export renders as the portable field again.
@@ -24113,6 +24170,13 @@ function importPassthrough(entry) {
24113
24170
  function resolveImportCommand(entry, logger) {
24114
24171
  const hasBash = typeof entry.bash === "string";
24115
24172
  const hasPowershell = typeof entry.powershell === "string";
24173
+ if (typeof entry.exec === "string") {
24174
+ if (hasBash || hasPowershell || typeof entry.command === "string") logger?.warn("Copilot CLI hook has both exec and a shell command; using exec and ignoring the shell fields, as the hooks reference says not to combine them.");
24175
+ return {
24176
+ command: entry.exec,
24177
+ args: entry.args ?? []
24178
+ };
24179
+ }
24116
24180
  if (hasBash && hasPowershell) {
24117
24181
  logger?.warn("Copilot CLI hook has both bash and powershell commands; using bash and ignoring powershell, so the imported config does not depend on the machine the import ran on.");
24118
24182
  return {
@@ -24162,11 +24226,12 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
24162
24226
  ...passthrough
24163
24227
  });
24164
24228
  else {
24165
- const { command, shell } = resolveImportCommand(entry, logger);
24229
+ const { command, shell, args } = resolveImportCommand(entry, logger);
24166
24230
  defs.push({
24167
24231
  type: "command",
24168
24232
  ...command !== void 0 && { command },
24169
24233
  ...shell !== void 0 && { shell },
24234
+ ...args !== void 0 && { args },
24170
24235
  ...matcherPart,
24171
24236
  ...timeoutPart,
24172
24237
  ...passthrough
@@ -24716,6 +24781,7 @@ var CursorHooks = class CursorHooks extends ToolHooks {
24716
24781
  ...def.loop_limit !== void 0 && { loop_limit: def.loop_limit },
24717
24782
  ...def.matcher !== void 0 && def.matcher !== null && { matcher: def.matcher },
24718
24783
  ...def.prompt !== void 0 && def.prompt !== null && { prompt: def.prompt },
24784
+ ...def.type === "prompt" && def.model !== void 0 && def.model !== null && { model: def.model },
24719
24785
  ...def.failClosed !== void 0 && def.failClosed !== null && { failClosed: def.failClosed }
24720
24786
  }));
24721
24787
  if (mappedDefs.length > 0) mappedHooks[cursorEventName] = mappedDefs;
@@ -59200,8 +59266,11 @@ const CursorSkillFrontmatterSchema = zod_mini.z.looseObject({
59200
59266
  name: zod_mini.z.string(),
59201
59267
  description: zod_mini.z.string(),
59202
59268
  paths: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
59269
+ globs: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
59203
59270
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
59204
59271
  "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
59272
+ icon: zod_mini.z.optional(zod_mini.z.string()),
59273
+ color: zod_mini.z.optional(zod_mini.z.string()),
59205
59274
  metadata: zod_mini.z.optional(zod_mini.z.looseObject({}))
59206
59275
  });
59207
59276
  /**
@@ -59253,10 +59322,13 @@ var CursorSkill = class CursorSkill extends ToolSkill {
59253
59322
  }
59254
59323
  toRulesyncSkill() {
59255
59324
  const frontmatter = this.getFrontmatter();
59325
+ const paths = frontmatter.paths ?? frontmatter.globs;
59256
59326
  const cursorSection = {
59257
- ...frontmatter.paths !== void 0 && { paths: frontmatter.paths },
59327
+ ...paths !== void 0 && { paths },
59258
59328
  ...frontmatter["disable-model-invocation"] !== void 0 && { "disable-model-invocation": frontmatter["disable-model-invocation"] },
59259
59329
  ...frontmatter["user-invocable"] !== void 0 && { "user-invocable": frontmatter["user-invocable"] },
59330
+ ...frontmatter.icon !== void 0 && { icon: frontmatter.icon },
59331
+ ...frontmatter.color !== void 0 && { color: frontmatter.color },
59260
59332
  ...frontmatter.metadata !== void 0 && { metadata: frontmatter.metadata }
59261
59333
  };
59262
59334
  const rulesyncFrontmatter = {
@@ -59298,6 +59370,8 @@ var CursorSkill = class CursorSkill extends ToolSkill {
59298
59370
  ...cursorSection?.paths !== void 0 && { paths: cursorSection.paths },
59299
59371
  ...resolvedDisableModelInvocation !== void 0 && { "disable-model-invocation": resolvedDisableModelInvocation },
59300
59372
  ...resolvedUserInvocable !== void 0 && { "user-invocable": resolvedUserInvocable },
59373
+ ...cursorSection?.icon !== void 0 && { icon: cursorSection.icon },
59374
+ ...cursorSection?.color !== void 0 && { color: cursorSection.color },
59301
59375
  ...metadata !== void 0 && { metadata }
59302
59376
  };
59303
59377
  return new CursorSkill({
@@ -66057,11 +66131,17 @@ var CopilotSubagent = class CopilotSubagent extends ToolSubagent {
66057
66131
  *
66058
66132
  * Reference: https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/create-custom-agents-for-cli
66059
66133
  * https://docs.github.com/en/copilot/reference/custom-agents-configuration
66134
+ * https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-command-reference
66060
66135
  *
66061
66136
  * Per the configuration docs:
66062
66137
  * - `description` is required.
66063
66138
  * - `name`, `target`, `tools`, `model`, `disable-model-invocation`,
66064
66139
  * `user-invocable`, `mcp-servers`, and `metadata` are optional.
66140
+ * - The CLI command reference adds `models` (authored models in priority
66141
+ * order, overriding `model` when both are set) and `modelPolicy`
66142
+ * (`preferred` lets a `subagents` settings override or the `/subagents`
66143
+ * picker replace the model; `required` locks dispatch to the authored
66144
+ * list). CLI v1.0.81+; the shared reference page still lists `model` only.
66065
66145
  *
66066
66146
  * `looseObject` is used so that future additional fields are passed through
66067
66147
  * unchanged (consistent with the rest of rulesync's frontmatter schemas).
@@ -66072,6 +66152,8 @@ const CopilotCliSubagentFrontmatterSchema = zod_mini.z.looseObject({
66072
66152
  target: zod_mini.z.optional(zod_mini.z.string()),
66073
66153
  tools: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.string(), zod_mini.z.array(zod_mini.z.string())])),
66074
66154
  model: zod_mini.z.optional(zod_mini.z.string()),
66155
+ models: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
66156
+ modelPolicy: zod_mini.z.optional(zod_mini.z.enum(["preferred", "required"])),
66075
66157
  "disable-model-invocation": zod_mini.z.optional(zod_mini.z.boolean()),
66076
66158
  "user-invocable": zod_mini.z.optional(zod_mini.z.boolean()),
66077
66159
  "mcp-servers": zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.unknown())),
@@ -70448,16 +70530,131 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
70448
70530
  }
70449
70531
  };
70450
70532
  //#endregion
70533
+ //#region src/utils/brace-aware-list.ts
70534
+ /**
70535
+ * Splits a comma-separated pattern list on the commas that separate its
70536
+ * entries, leaving the commas inside a brace group alone: in
70537
+ * `{src,lib}/**\/*.ts, tests/**\/*.test.ts` the first comma belongs to the
70538
+ * brace expansion and the second separates the two globs. Entries are trimmed;
70539
+ * empty entries are dropped.
70540
+ *
70541
+ * @example
70542
+ * splitBraceAwareList("src/**\/*.{ts,tsx}, docs/**")
70543
+ * // => ["src/**\/*.{ts,tsx}", "docs/**"]
70544
+ */
70545
+ const splitBraceAwareList = (value) => {
70546
+ const entries = [];
70547
+ let current = "";
70548
+ let braceDepth = 0;
70549
+ for (const char of value) {
70550
+ if (char === "{") braceDepth += 1;
70551
+ else if (char === "}" && braceDepth > 0) braceDepth -= 1;
70552
+ else if (char === "," && braceDepth === 0) {
70553
+ entries.push(current);
70554
+ current = "";
70555
+ continue;
70556
+ }
70557
+ current += char;
70558
+ }
70559
+ entries.push(current);
70560
+ return entries.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
70561
+ };
70562
+ /**
70563
+ * Upper bound on the patterns one glob may expand to; past it the glob is
70564
+ * returned verbatim, since `{a,b}` repeated k times yields 2^k branches.
70565
+ */
70566
+ const MAX_BRACE_EXPANSIONS = 256;
70567
+ /** Matches an innermost brace group with at least one comma: `{a,b}`, not `{a}` or `{a,{b,c}}`. */
70568
+ const INNERMOST_BRACE_GROUP_REGEX = /\{([^{},]*(?:,[^{},]*)+)\}/;
70569
+ /**
70570
+ * Expands the comma alternations of a glob (`a.{ts,tsx}` → `a.ts`, `a.tsx`)
70571
+ * into one pattern per combination, innermost group first, so the result can
70572
+ * be handed to a consumer that splits its pattern list on bare commas. A glob
70573
+ * without a brace alternation is returned as is; a brace group without a
70574
+ * comma (e.g. `{a}`) is left untouched. A glob whose expansion would exceed
70575
+ * `MAX_BRACE_EXPANSIONS` patterns is returned as is as well.
70576
+ *
70577
+ * The cap is enforced on the work list itself rather than by counting the
70578
+ * groups up front: a nested group such as `{{a,b},c}` is rewritten into one
70579
+ * new group per inner alternative, so the number of patterns in flight is the
70580
+ * only bound that holds for arbitrary nesting.
70581
+ *
70582
+ * @example
70583
+ * expandBraceAlternations("src/**\/*.{ts,tsx}")
70584
+ * // => ["src/**\/*.ts", "src/**\/*.tsx"]
70585
+ */
70586
+ const expandBraceAlternations = (glob) => {
70587
+ let pending = [glob];
70588
+ for (;;) {
70589
+ const next = [];
70590
+ let expanded = false;
70591
+ for (const pattern of pending) {
70592
+ const match = INNERMOST_BRACE_GROUP_REGEX.exec(pattern);
70593
+ if (match === null) {
70594
+ next.push(pattern);
70595
+ continue;
70596
+ }
70597
+ expanded = true;
70598
+ const prefix = pattern.slice(0, match.index);
70599
+ const suffix = pattern.slice(match.index + match[0].length);
70600
+ for (const alternative of (match[1] ?? "").split(",")) next.push(`${prefix}${alternative}${suffix}`);
70601
+ if (next.length > MAX_BRACE_EXPANSIONS) return [glob];
70602
+ }
70603
+ if (!expanded) return [...new Set(next)];
70604
+ pending = next;
70605
+ }
70606
+ };
70607
+ //#endregion
70451
70608
  //#region src/features/rules/aiassistant-rule.ts
70452
70609
  /**
70610
+ * Rule types AI Assistant recognizes on the `apply:` metadata line. The values
70611
+ * are matched case-sensitively by the plugin, so they are emitted verbatim.
70612
+ * Any other value is carried through untouched (the plugin treats it as Off
70613
+ * today; a future build may recognize it).
70614
+ */
70615
+ const AIASSISTANT_APPLY_VALUES = [
70616
+ "always",
70617
+ "manually",
70618
+ "by model decision",
70619
+ "by file patterns",
70620
+ "off"
70621
+ ];
70622
+ const isAiassistantApply = (value) => AIASSISTANT_APPLY_VALUES.includes(value);
70623
+ /**
70624
+ * Globs that match the whole project and so say nothing beyond "always".
70625
+ */
70626
+ const UNIVERSAL_GLOBS$2 = /* @__PURE__ */ new Set(["**/*", "*"]);
70627
+ const METADATA_DELIMITER = "---";
70628
+ /**
70629
+ * Splits text into lines that keep their own line terminator, so the body can
70630
+ * be sliced back out of the file byte-for-byte. Lone `\r` counts as a break,
70631
+ * matching Kotlin's `lines()`, which the plugin uses on the block.
70632
+ */
70633
+ const LINE_SPLIT_REGEX = /(?<=\r\n|\r(?!\n)|(?<!\r)\n)/;
70634
+ /**
70635
+ * The block is line-based, so a value must not carry a line break: one would
70636
+ * start a new `<field>: <value>` line and could override `apply:`.
70637
+ */
70638
+ const flattenValue = (value) => value.split(/\r\n|\r|\n/).map((part) => part.trim()).filter((part) => part.length > 0).join(" ");
70639
+ /**
70453
70640
  * Rule generator for JetBrains AI Assistant.
70454
70641
  *
70455
70642
  * AI Assistant reads project rules as flat Markdown files in
70456
- * `.aiassistant/rules/*.md` (one file per rule, identified by filename). The rule
70457
- * type (Always / Manually / By model decision / By file patterns) is configured
70458
- * in the IDE, not in the file, so the generated files are plain Markdown with no
70459
- * frontmatter. There is no special root file (unlike Junie's `guidelines.md`), so
70460
- * every rule — root or non-root — is written as its own file under `rules/`.
70643
+ * `.aiassistant/rules/*.md` (one file per rule, identified by filename). There
70644
+ * is no special root file (unlike Junie's `guidelines.md`), so every rule —
70645
+ * root or non-root — is written as its own file under `rules/`.
70646
+ *
70647
+ * The rule type (Always / Manually / By model decision / By file patterns /
70648
+ * Off) is stored in the file itself as a leading `---` metadata block that the
70649
+ * IDE's "Rule type" selector edits in place. The block is not YAML: each line
70650
+ * is `<field>: <value>` split on the first colon, and `patterns` is a
70651
+ * comma-separated glob list, so it is written and parsed by hand here. A file
70652
+ * without an `apply:` line is treated as Off at retrieval time (it is never
70653
+ * attached automatically), which is why every generated file carries one.
70654
+ *
70655
+ * Field names and values come from the plugin (`AiRulesService`,
70656
+ * `AiRulesLLMBundle.properties`; verified on build 262.10968.75) — the docs
70657
+ * page describes the rule type only as an IDE setting.
70461
70658
  *
70462
70659
  * AI Assistant and Junie are different JetBrains products (real-time assistance
70463
70660
  * vs. autonomous agent) with different layouts, so this is a separate target.
@@ -70465,18 +70662,154 @@ var AgentsMdRule = class AgentsMdRule extends ToolRule {
70465
70662
  * @see https://www.jetbrains.com/help/ai-assistant/configure-project-rules.html
70466
70663
  */
70467
70664
  var AiassistantRule = class AiassistantRule extends ToolRule {
70665
+ body;
70666
+ metadata;
70667
+ constructor({ body, metadata, ...rest }) {
70668
+ super({
70669
+ ...rest,
70670
+ fileContent: AiassistantRule.buildFileContent({
70671
+ body,
70672
+ metadata
70673
+ })
70674
+ });
70675
+ this.body = body;
70676
+ this.metadata = metadata;
70677
+ }
70468
70678
  static getSettablePaths(_options = {}) {
70469
70679
  return { nonRoot: { relativeDirPath: AIASSISTANT_RULES_DIR_PATH } };
70470
70680
  }
70681
+ getBody() {
70682
+ return this.body;
70683
+ }
70684
+ getMetadata() {
70685
+ return this.metadata;
70686
+ }
70687
+ /**
70688
+ * Serialize the metadata block the way the IDE writes it
70689
+ * (`---\n<field>: <value>\n---`), followed by the body.
70690
+ */
70691
+ static buildFileContent({ body, metadata }) {
70692
+ if (metadata === void 0) return body;
70693
+ const lines = [`apply: ${metadata.apply}`];
70694
+ if (metadata.apply === "by model decision" && metadata.instructions) lines.push(`instructions: ${flattenValue(metadata.instructions)}`);
70695
+ if (metadata.apply === "by file patterns" && metadata.patterns && metadata.patterns.length > 0) lines.push(`patterns: ${metadata.patterns.map(flattenValue).join(", ")}`);
70696
+ return `${METADATA_DELIMITER}\n${lines.join("\n")}\n${METADATA_DELIMITER}\n\n${body}`;
70697
+ }
70698
+ /**
70699
+ * Split a rule file into its metadata block and body. The plugin locates the
70700
+ * block with `^---\s*([\s\S]*?)---\s*` (anchored at the very start of the
70701
+ * file); this reads it line by line instead — a delimiter is a line that is
70702
+ * exactly `---` — which is linear on any input and tolerates a BOM or blank
70703
+ * lines before the block. Each line is split on its first colon and trimmed;
70704
+ * unknown fields are ignored; a missing block or `apply:` line yields no
70705
+ * metadata, while an unrecognized `apply:` value is kept verbatim.
70706
+ */
70707
+ static parseFileContent(fileContent) {
70708
+ const content = fileContent.replace(/^\uFEFF/, "").trimStart();
70709
+ const lines = content.split(LINE_SPLIT_REGEX);
70710
+ const closingIndex = lines[0]?.trim() === METADATA_DELIMITER ? lines.findIndex((line, index) => index > 0 && line.trim() === METADATA_DELIMITER) : -1;
70711
+ if (closingIndex === -1) return {
70712
+ metadata: void 0,
70713
+ body: content.trim()
70714
+ };
70715
+ const body = lines.slice(closingIndex + 1).join("").trim();
70716
+ const fields = /* @__PURE__ */ new Map();
70717
+ for (const line of lines.slice(1, closingIndex)) {
70718
+ const separatorIndex = line.indexOf(":");
70719
+ if (separatorIndex === -1) continue;
70720
+ fields.set(line.slice(0, separatorIndex).trim(), line.slice(separatorIndex + 1).trim());
70721
+ }
70722
+ const apply = fields.get("apply");
70723
+ if (apply === void 0 || apply.length === 0) return {
70724
+ metadata: void 0,
70725
+ body
70726
+ };
70727
+ const instructions = fields.get("instructions");
70728
+ const patterns = fields.get("patterns");
70729
+ return {
70730
+ metadata: {
70731
+ apply,
70732
+ ...apply === "by model decision" && instructions && { instructions },
70733
+ ...apply === "by file patterns" && patterns && { patterns: splitBraceAwareList(patterns) }
70734
+ },
70735
+ body
70736
+ };
70737
+ }
70738
+ /**
70739
+ * Derive the metadata block from the canonical frontmatter, mirroring the
70740
+ * Cursor mapping: an explicit `aiassistant.apply` wins; otherwise specific
70741
+ * `globs` become `by file patterns`, a `description` on a rule with no
70742
+ * `globs` at all becomes the `by model decision` condition, and everything
70743
+ * else — the root rule, universal globs (which already say "every file",
70744
+ * whatever the description), bare rules — is `always`.
70745
+ *
70746
+ * The plugin splits `patterns` on every comma, so a brace alternation such
70747
+ * as `*.{ts,tsx}` is expanded into one glob per branch before it is written.
70748
+ */
70749
+ static buildMetadata(frontmatter) {
70750
+ const globs = (frontmatter.globs ?? []).map((glob) => flattenValue(glob)).filter((glob) => glob.length > 0).flatMap((glob) => expandBraceAlternations(glob));
70751
+ const hasUniversalGlob = globs.some((glob) => UNIVERSAL_GLOBS$2.has(glob));
70752
+ const instructions = frontmatter.description && flattenValue(frontmatter.description);
70753
+ const explicitApply = frontmatter.aiassistant?.apply && flattenValue(frontmatter.aiassistant.apply);
70754
+ const apply = explicitApply ? explicitApply : frontmatter.root !== true && globs.length > 0 && !hasUniversalGlob ? "by file patterns" : frontmatter.root !== true && globs.length === 0 && instructions ? "by model decision" : "always";
70755
+ const patterns = globs;
70756
+ return {
70757
+ apply,
70758
+ ...apply === "by model decision" && instructions && { instructions },
70759
+ ...apply === "by file patterns" && patterns.length > 0 && { patterns }
70760
+ };
70761
+ }
70762
+ /**
70763
+ * Warn, at generate time, about a block the plugin cannot act on as the
70764
+ * author presumably intended.
70765
+ */
70766
+ static warnAboutMetadata({ metadata, relativeFilePath }) {
70767
+ const { apply, instructions, patterns = [] } = metadata;
70768
+ if (!isAiassistantApply(apply)) warnWithFallback(void 0, `${relativeFilePath}: aiassistant.apply "${apply}" is not one of ${AIASSISTANT_APPLY_VALUES.join(", ")}; it is written as is, and the current AI Assistant treats such a rule as Off.`);
70769
+ if (apply === "by file patterns" && patterns.length === 0) warnWithFallback(void 0, `${relativeFilePath}: aiassistant.apply is "by file patterns" but the rule has no globs, so AI Assistant will never attach it.`);
70770
+ if (apply === "by model decision" && !instructions) warnWithFallback(void 0, `${relativeFilePath}: aiassistant.apply is "by model decision" but the rule has no description, so AI Assistant has no condition to decide on.`);
70771
+ const commaGlob = patterns.find((glob) => glob.includes(","));
70772
+ if (commaGlob !== void 0) warnWithFallback(void 0, `${relativeFilePath}: the glob "${commaGlob}" contains a comma, which AI Assistant reads as a pattern separator.`);
70773
+ if ([
70774
+ apply,
70775
+ instructions ?? "",
70776
+ ...patterns
70777
+ ].some((value) => value.includes("---"))) warnWithFallback(void 0, `${relativeFilePath}: a metadata value contains "---", which AI Assistant reads as the end of the metadata block.`);
70778
+ }
70471
70779
  toRulesyncRule() {
70472
- return this.toRulesyncRuleDefault();
70780
+ const metadata = this.metadata;
70781
+ const globs = metadata?.apply === "by file patterns" ? metadata.patterns ?? [] : metadata?.apply === "always" ? ["**/*"] : [];
70782
+ const frontmatter = {
70783
+ root: false,
70784
+ targets: ["*"],
70785
+ description: metadata?.apply === "by model decision" ? metadata.instructions : void 0,
70786
+ globs
70787
+ };
70788
+ const derivedApply = metadata === void 0 ? void 0 : AiassistantRule.buildMetadata(frontmatter).apply;
70789
+ const apply = metadata !== void 0 && derivedApply !== metadata.apply ? metadata.apply : void 0;
70790
+ return new RulesyncRule({
70791
+ outputRoot: process.cwd(),
70792
+ relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
70793
+ relativeFilePath: this.getRelativeFilePath(),
70794
+ frontmatter: {
70795
+ ...frontmatter,
70796
+ ...apply !== void 0 && { aiassistant: { apply } }
70797
+ },
70798
+ body: this.body
70799
+ });
70473
70800
  }
70474
70801
  static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
70802
+ const metadata = this.buildMetadata(rulesyncRule.getFrontmatter());
70803
+ this.warnAboutMetadata({
70804
+ metadata,
70805
+ relativeFilePath: rulesyncRule.getRelativeFilePath()
70806
+ });
70475
70807
  return new AiassistantRule({
70476
70808
  outputRoot,
70477
70809
  relativeDirPath: this.getSettablePaths().nonRoot.relativeDirPath,
70478
70810
  relativeFilePath: rulesyncRule.getRelativeFilePath(),
70479
- fileContent: rulesyncRule.getBody(),
70811
+ body: rulesyncRule.getBody(),
70812
+ metadata,
70480
70813
  validate,
70481
70814
  root: false
70482
70815
  });
@@ -70484,11 +70817,13 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
70484
70817
  static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
70485
70818
  const relativeDirPath = this.getSettablePaths().nonRoot.relativeDirPath;
70486
70819
  const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath));
70820
+ const { metadata, body } = this.parseFileContent(fileContent);
70487
70821
  return new AiassistantRule({
70488
70822
  outputRoot,
70489
70823
  relativeDirPath,
70490
70824
  relativeFilePath,
70491
- fileContent: fileContent.trim(),
70825
+ body,
70826
+ metadata,
70492
70827
  validate,
70493
70828
  root: false
70494
70829
  });
@@ -70498,7 +70833,7 @@ var AiassistantRule = class AiassistantRule extends ToolRule {
70498
70833
  outputRoot,
70499
70834
  relativeDirPath,
70500
70835
  relativeFilePath,
70501
- fileContent: "",
70836
+ body: "",
70502
70837
  validate: false,
70503
70838
  root: false
70504
70839
  });
@@ -72055,40 +72390,14 @@ const CodebuddyRuleFrontmatterSchema = zod_mini.z.object({
72055
72390
  enabled: zod_mini.z.optional(zod_mini.z.boolean())
72056
72391
  });
72057
72392
  /**
72058
- * Splits a scalar `paths` value on the commas that separate patterns, leaving
72059
- * the commas inside a brace group alone: CodeBuddy's memory docs say "You can
72060
- * also combine multiple patterns with commas" and give
72061
- * `paths: {src,lib}/**\/*.ts, tests/**\/*.test.ts` as the example, where the
72062
- * first comma belongs to the brace expansion and the second separates the two
72063
- * globs. Only a scalar is split — the docs describe the comma form for the
72064
- * string shape, and a list already separates its patterns.
72065
- */
72066
- function splitCodebuddyPathsScalar(paths) {
72067
- const patterns = [];
72068
- let current = "";
72069
- let braceDepth = 0;
72070
- for (const char of paths) {
72071
- if (char === "{") braceDepth += 1;
72072
- else if (char === "}" && braceDepth > 0) braceDepth -= 1;
72073
- else if (char === "," && braceDepth === 0) {
72074
- patterns.push(current);
72075
- current = "";
72076
- continue;
72077
- }
72078
- current += char;
72079
- }
72080
- patterns.push(current);
72081
- return patterns.map((pattern) => pattern.trim());
72082
- }
72083
- /**
72084
72393
  * Normalizes the documented `string` / `string[]` shapes of `paths` to the
72085
72394
  * list form the rest of the adapter works with, splitting a comma-separated
72086
- * scalar into its patterns (see `splitCodebuddyPathsScalar`). An empty list
72395
+ * scalar into its patterns (see `splitBraceAwareList`). An empty list
72087
72396
  * and an empty string both mean "no paths".
72088
72397
  */
72089
72398
  function normalizeCodebuddyPaths(paths) {
72090
72399
  if (paths === void 0) return;
72091
- const list = (typeof paths === "string" ? splitCodebuddyPathsScalar(paths) : paths).filter((path) => path.trim() !== "");
72400
+ const list = (typeof paths === "string" ? splitBraceAwareList(paths) : paths).filter((path) => path.trim() !== "");
72092
72401
  return list.length > 0 ? list : void 0;
72093
72402
  }
72094
72403
  /**
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_import = require("./import-D_daTMgR.cjs");
2
+ const require_import = require("./import-xo5me5dm.cjs");
3
3
  //#region src/index.ts
4
4
  async function generate(options = {}) {
5
5
  const { silent = true, verbose = false, ...rest } = options;
package/dist/index.d.cts CHANGED
@@ -499,6 +499,8 @@ declare const RulesyncSkillFrontmatterSchemaInternal: z.ZodMiniObject<{
499
499
  paths: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniArray<z.ZodMiniString<string>>]>>;
500
500
  "disable-model-invocation": z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
501
501
  "user-invocable": z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
502
+ icon: z.ZodMiniOptional<z.ZodMiniString<string>>;
503
+ color: z.ZodMiniOptional<z.ZodMiniString<string>>;
502
504
  metadata: z.ZodMiniOptional<z.ZodMiniObject<{}, z.core.$loose>>;
503
505
  }, z.core.$loose>>;
504
506
  factorydroid: z.ZodMiniOptional<z.ZodMiniObject<{
@@ -725,6 +727,8 @@ type RulesyncSkillFrontmatterInput = {
725
727
  paths?: string | string[];
726
728
  "disable-model-invocation"?: boolean;
727
729
  "user-invocable"?: boolean;
730
+ icon?: string;
731
+ color?: string;
728
732
  metadata?: Record<string, unknown>;
729
733
  };
730
734
  factorydroid?: {