rulesync 32.0.0 → 33.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.
@@ -21766,22 +21766,29 @@ function mergeOpencodeV2Entries({ config, legacyKey, pluralKey, lower }) {
21766
21766
  const entry = asOpencodeEntries(value);
21767
21767
  const lowered = entry === null ? null : lower(entry);
21768
21768
  if (Object.hasOwn(merged, name) || lowered === null) continue;
21769
- Object.defineProperty(merged, name, {
21770
- value: lowered,
21771
- enumerable: true,
21772
- configurable: true,
21773
- writable: true
21774
- });
21769
+ setOwnEntry(merged, name, lowered);
21775
21770
  }
21776
21771
  return Object.keys(merged).length > 0 ? merged : legacy;
21777
21772
  }
21778
21773
  /**
21774
+ * Sets `target[key]` as an own, enumerable property. A plain assignment to a
21775
+ * key named `__proto__` would set the prototype instead.
21776
+ */
21777
+ function setOwnEntry(target, key, value) {
21778
+ Object.defineProperty(target, key, {
21779
+ value,
21780
+ enumerable: true,
21781
+ configurable: true,
21782
+ writable: true
21783
+ });
21784
+ }
21785
+ /**
21779
21786
  * Returns the inline agent entries of an OpenCode config in the V1 `agent`
21780
21787
  * shape. Since v1.18.24 OpenCode V1 also reads the V2 `agents` spelling and
21781
21788
  * lowers it, so a layout that does so (`readsV2ConfigSpellings`) gets those
21782
21789
  * entries too.
21783
21790
  *
21784
- * @see https://github.com/anomalyco/opencode/blob/v1.18.34/packages/opencode/src/config/v2-compat.ts
21791
+ * @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
21785
21792
  */
21786
21793
  function getOpencodeAgentEntries({ config, layout = OPENCODE_LAYOUT }) {
21787
21794
  if (!layout.readsV2ConfigSpellings) return asOpencodeEntries(config.agent);
@@ -21800,7 +21807,7 @@ function getOpencodeAgentEntries({ config, layout = OPENCODE_LAYOUT }) {
21800
21807
  * Other fields are kept: V1 drops a V2 `subagent`, but rulesync reads it as
21801
21808
  * `subtask` everywhere (as V2 does), so it is left for the command importer.
21802
21809
  *
21803
- * @see https://github.com/anomalyco/opencode/blob/v1.18.34/packages/opencode/src/config/v2-compat.ts
21810
+ * @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
21804
21811
  */
21805
21812
  function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
21806
21813
  if (!layout.readsV2ConfigSpellings) return asOpencodeEntries(config.command);
@@ -21823,7 +21830,7 @@ function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
21823
21830
  * reads V2 spellings, a flat V2 `skills` array is accepted too: its entries
21824
21831
  * that are not `http(s)://` URLs are the paths, as OpenCode V1 lowers them.
21825
21832
  *
21826
- * @see https://github.com/anomalyco/opencode/blob/v1.18.34/packages/opencode/src/config/v2-compat.ts
21833
+ * @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
21827
21834
  */
21828
21835
  function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
21829
21836
  if (Array.isArray(config.skills)) {
@@ -21833,6 +21840,160 @@ function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
21833
21840
  const skills = asOpencodeEntries(config.skills);
21834
21841
  return skills !== null && Array.isArray(skills.paths) ? skills.paths : [];
21835
21842
  }
21843
+ /**
21844
+ * Whether a record is itself an MCP server entry rather than a V2 container
21845
+ * (`mcp.servers`, `mcp.timeout`): a server — or a server literally named
21846
+ * `type` / `enabled` — carries a scalar `type` or `enabled`. Mirrors OpenCode
21847
+ * V1's `isDirectServer`.
21848
+ */
21849
+ function isDirectOpencodeMcpServer(value) {
21850
+ return ["type", "enabled"].some((key) => Object.hasOwn(value, key) && (value[key] === null || typeof value[key] !== "object" || Array.isArray(value[key])));
21851
+ }
21852
+ const OPENCODE_V2_TIMEOUT_KEYS = [
21853
+ "startup",
21854
+ "catalog",
21855
+ "execution"
21856
+ ];
21857
+ /**
21858
+ * Whether an MCP server entry uses a V2-only per-server spelling, so that
21859
+ * OpenCode V1 lowers it: `disabled`, `codemode`, an object `timeout`, or a
21860
+ * snake_case `oauth` key.
21861
+ */
21862
+ function isOpencodeV2NativeServer(value) {
21863
+ const oauth = value.oauth;
21864
+ return Object.hasOwn(value, "disabled") || Object.hasOwn(value, "codemode") || isRecord$1(value.timeout) || isRecord$1(oauth) && [
21865
+ "client_id",
21866
+ "client_secret",
21867
+ "callback_port",
21868
+ "redirect_uri"
21869
+ ].some((key) => Object.hasOwn(oauth, key));
21870
+ }
21871
+ /**
21872
+ * Lowers a V2 timeout object (`{ startup, catalog, execution }`) to V1's single
21873
+ * number, the way OpenCode V1 does: only a `catalog` equal to `execution`, with
21874
+ * no `startup`, has a V1 equivalent. Anything else — including a value that is
21875
+ * not a positive integer — is `undefined` (dropped).
21876
+ */
21877
+ function lowerOpencodeV2Timeout(value) {
21878
+ if (!hasPositiveIntegerTimeouts(value) || value.startup !== void 0 || typeof value.catalog !== "number") return;
21879
+ return value.catalog === value.execution ? value.catalog : void 0;
21880
+ }
21881
+ const OPENCODE_V2_OAUTH_KEYS = /* @__PURE__ */ new Map([
21882
+ ["client_id", "clientId"],
21883
+ ["client_secret", "clientSecret"],
21884
+ ["callback_port", "callbackPort"],
21885
+ ["redirect_uri", "redirectUri"]
21886
+ ]);
21887
+ /**
21888
+ * V2 `oauth` keys that have no V1 spelling. OpenCode V1 drops them when it
21889
+ * lowers a server, so they are dropped (and reported) here too rather than
21890
+ * being written back as a snake_case key among camelCase ones.
21891
+ */
21892
+ const OPENCODE_V2_UNSUPPORTED_OAUTH_KEYS = /* @__PURE__ */ new Set(["auth_server_metadata_url"]);
21893
+ function lowerOpencodeV2OAuth(oauth, path, onDropped) {
21894
+ const lowered = {};
21895
+ for (const [key, value] of Object.entries(oauth)) {
21896
+ if (OPENCODE_V2_UNSUPPORTED_OAUTH_KEYS.has(key)) {
21897
+ onDropped?.(`${path}.${key}`, "unsupported");
21898
+ continue;
21899
+ }
21900
+ setOwnEntry(lowered, OPENCODE_V2_OAUTH_KEYS.get(key) ?? key, value);
21901
+ }
21902
+ return lowered;
21903
+ }
21904
+ /**
21905
+ * Lowers one V2-spelled MCP server entry to V1's shape, as OpenCode V1 does:
21906
+ * `disabled` becomes `enabled` (an explicit `enabled` wins), an object
21907
+ * `timeout` becomes a number or is dropped, and snake_case `oauth` keys become
21908
+ * camelCase. `codemode` is kept: Rulesync passes it through where V1 drops it.
21909
+ *
21910
+ * Like V1, only an entry that is a local or remote server is lowered, and one
21911
+ * whose `enabled` or `disabled` is not a boolean is left as it is. Unlike V1,
21912
+ * such an entry is not dropped but passed on, so the schema rejects it loudly.
21913
+ */
21914
+ function lowerOpencodeV2Server(value, path, onDropped) {
21915
+ if (!isRecord$1(value) || value.type !== "local" && value.type !== "remote" || Object.hasOwn(value, "enabled") && typeof value.enabled !== "boolean" || Object.hasOwn(value, "disabled") && typeof value.disabled !== "boolean") return value;
21916
+ const { disabled, timeout, ...rest } = value;
21917
+ const lowered = { ...rest };
21918
+ if (!Object.hasOwn(value, "enabled")) lowered.enabled = disabled !== true;
21919
+ else if (value.enabled === disabled) onDropped?.(`${path}.disabled`, "conflict");
21920
+ const lowTimeout = isRecord$1(timeout) ? lowerOpencodeV2Timeout(timeout) : timeout;
21921
+ if (lowTimeout !== void 0) lowered.timeout = lowTimeout;
21922
+ else if (isRecord$1(timeout) && Object.keys(timeout).length > 0) onDropped?.(`${path}.timeout`, "unsupported");
21923
+ if (isRecord$1(value.oauth)) lowered.oauth = lowerOpencodeV2OAuth(value.oauth, `${path}.oauth`, onDropped);
21924
+ return lowered;
21925
+ }
21926
+ function isPositiveInteger(value) {
21927
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
21928
+ }
21929
+ /** Whether each V2 timeout value a record carries is a positive integer. */
21930
+ function hasPositiveIntegerTimeouts(value) {
21931
+ return OPENCODE_V2_TIMEOUT_KEYS.every((key) => value[key] === void 0 || isPositiveInteger(value[key]));
21932
+ }
21933
+ /**
21934
+ * Whether `mcp.timeout` is V2's global timeout object rather than a server
21935
+ * named `timeout`. As in OpenCode V1, each of its values must be a positive
21936
+ * integer; anything else is read as a server, which the schema then rejects.
21937
+ */
21938
+ function isOpencodeV2GlobalTimeout(value) {
21939
+ return isRecord$1(value) && !isDirectOpencodeMcpServer(value) && (Object.keys(value).length === 0 || OPENCODE_V2_TIMEOUT_KEYS.some((key) => Object.hasOwn(value, key))) && hasPositiveIntegerTimeouts(value);
21940
+ }
21941
+ function getOpencodeV2McpEnvelope(mcp) {
21942
+ const nested = mcp.servers;
21943
+ return isRecord$1(nested) && !isDirectOpencodeMcpServer(nested) ? nested : null;
21944
+ }
21945
+ /**
21946
+ * Returns the paths of the V2-only containers in an OpenCode `mcp` value that
21947
+ * a write in V1's flat shape does not keep: a non-empty `mcp.servers`
21948
+ * envelope, and a non-empty global `mcp.timeout`.
21949
+ */
21950
+ function findOpencodeV2McpContainers(mcp) {
21951
+ if (!isRecord$1(mcp)) return [];
21952
+ const envelope = getOpencodeV2McpEnvelope(mcp);
21953
+ return [...envelope !== null && Object.keys(envelope).length > 0 ? ["mcp.servers"] : [], ...isOpencodeV2GlobalTimeout(mcp.timeout) && Object.keys(mcp.timeout).length > 0 ? ["mcp.timeout"] : []];
21954
+ }
21955
+ /**
21956
+ * Lowers OpenCode V2 `mcp` spellings found in a V1 config to the flat V1 map,
21957
+ * the way OpenCode V1 itself does since v1.18.24 (`normalizeMcp`):
21958
+ *
21959
+ * - entries of the `mcp.servers` envelope are merged into the map, a flat entry
21960
+ * of the same name winning;
21961
+ * - V2-spelled server entries (`disabled`, an object `timeout`, snake_case
21962
+ * `oauth` keys) are lowered by {@link lowerOpencodeV2Server};
21963
+ * - the global `mcp.timeout` object is skipped. V1 moves it to
21964
+ * `experimental.mcp_timeout`, a default request timeout that has no
21965
+ * canonical counterpart, so it is not read as a server either.
21966
+ *
21967
+ * Unlike V1, an envelope entry that is not a valid server is not dropped: it
21968
+ * is passed on, so the schema rejects it loudly rather than it vanishing
21969
+ * without a word. Each value that is dropped — a non-empty global timeout, a
21970
+ * per-server timeout object or `oauth` key with no V1 equivalent, or an
21971
+ * envelope entry shadowed by a different flat entry — is reported through
21972
+ * `onDropped`.
21973
+ *
21974
+ * @see https://github.com/anomalyco/opencode/blob/v1.18.35/packages/opencode/src/config/v2-compat.ts
21975
+ * @see https://opencode.ai/v2/docs/mcp-servers/
21976
+ */
21977
+ function lowerOpencodeV2Mcp(mcp, onDropped) {
21978
+ if (!isRecord$1(mcp)) return mcp;
21979
+ const envelope = getOpencodeV2McpEnvelope(mcp);
21980
+ const globalTimeout = isOpencodeV2GlobalTimeout(mcp.timeout);
21981
+ if (findOpencodeV2McpContainers(mcp).includes("mcp.timeout")) onDropped?.("mcp.timeout", "unsupported");
21982
+ const servers = {};
21983
+ for (const [name, value] of Object.entries(mcp)) {
21984
+ if (name === "servers" && envelope !== null || name === "timeout" && globalTimeout) continue;
21985
+ setOwnEntry(servers, name, isRecord$1(value) && isOpencodeV2NativeServer(value) ? lowerOpencodeV2Server(value, `mcp.${name}`, onDropped) : value);
21986
+ }
21987
+ for (const [name, value] of Object.entries(envelope ?? {})) {
21988
+ const path = `mcp.servers.${name}`;
21989
+ if (Object.hasOwn(servers, name)) {
21990
+ if (!(0, node_util.isDeepStrictEqual)(servers[name], value)) onDropped?.(path, "conflict");
21991
+ continue;
21992
+ }
21993
+ setOwnEntry(servers, name, lowerOpencodeV2Server(value, path, onDropped));
21994
+ }
21995
+ return servers;
21996
+ }
21836
21997
  /** Matches a string whose entire value is an OpenCode `{file:...}` reference. */
21837
21998
  const OPENCODE_FILE_TEMPLATE_PATTERN = /^\s*\{file:(.+?)\}\s*$/;
21838
21999
  /**
@@ -45581,18 +45742,21 @@ var KiroMcp = class KiroMcp extends ToolMcp {
45581
45742
  };
45582
45743
  //#endregion
45583
45744
  //#region src/features/mcp/opencode-mcp.ts
45745
+ const OpencodeMcpV2DisabledSchema = zod_mini.z.optional(zod_mini.z.boolean());
45584
45746
  const OpencodeMcpLocalServerSchema = zod_mini.z.looseObject({
45585
45747
  type: zod_mini.z.literal("local"),
45586
45748
  command: zod_mini.z.array(zod_mini.z.string()),
45587
45749
  environment: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
45588
45750
  enabled: zod_mini.z._default(zod_mini.z.boolean(), true),
45751
+ disabled: OpencodeMcpV2DisabledSchema,
45589
45752
  cwd: zod_mini.z.optional(zod_mini.z.string())
45590
45753
  });
45591
45754
  const OpencodeMcpRemoteServerSchema = zod_mini.z.looseObject({
45592
45755
  type: zod_mini.z.literal("remote"),
45593
45756
  url: zod_mini.z.string(),
45594
45757
  headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
45595
- enabled: zod_mini.z._default(zod_mini.z.boolean(), true)
45758
+ enabled: zod_mini.z._default(zod_mini.z.boolean(), true),
45759
+ disabled: OpencodeMcpV2DisabledSchema
45596
45760
  });
45597
45761
  const OPENCODE_PASSTHROUGH_SERVER_FIELDS = [
45598
45762
  "timeout",
@@ -45604,7 +45768,7 @@ const OPENCODE_MCP_TRANSPORT_KEYS = [
45604
45768
  ...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
45605
45769
  ...Object.keys(OpencodeMcpRemoteServerSchema.def.shape),
45606
45770
  ...OPENCODE_PASSTHROUGH_SERVER_FIELDS
45607
- ].filter((key) => key !== "enabled");
45771
+ ].filter((key) => key !== "enabled" && key !== "disabled");
45608
45772
  /**
45609
45773
  * A bare toggle entry: `{"enabled": <bool>}` with no transport of its own,
45610
45774
  * disabling a server another config layer defines. It is the third member of
@@ -45732,7 +45896,7 @@ const OPENCODE_TOGGLE_KEPT_KEYS = /* @__PURE__ */ new Set([
45732
45896
  function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
45733
45897
  const dropped = Object.keys(serverConfig).filter((key) => !OPENCODE_TOGGLE_KEPT_KEYS.has(key));
45734
45898
  if (dropped.length === 0) return;
45735
- logger?.warn(`OpenCode MCP: "${serverName}" declares no transport, so it is written as a toggle entry and ${dropped.toSorted().join(", ")} ${dropped.length === 1 ? "is" : "are"} dropped.`);
45899
+ logger?.warn(`OpenCode MCP: ${quoteValueForWarning(serverName)} declares no transport, so it is written as a toggle entry and ${dropped.toSorted().map(quoteValueForWarning).join(", ")} ${dropped.length === 1 ? "is" : "are"} dropped.`);
45736
45900
  }
45737
45901
  /**
45738
45902
  * Convert standard MCP format to OpenCode native format
@@ -45812,9 +45976,35 @@ var OpencodeMcp = class extends ToolMcp {
45812
45976
  /** Directory layout; OpenCode forks (MiMo Code) override it. */
45813
45977
  static layout = OPENCODE_LAYOUT;
45814
45978
  json;
45815
- constructor(params) {
45979
+ /**
45980
+ * With a `logger`, the V2 values that lowering the content drops are warned
45981
+ * about (see {@link OpencodeMcp.lowerConfig}); `fromFile` passes one so the
45982
+ * warnings come from the lowering that builds the parsed view rather than
45983
+ * from an extra pass of their own. (`validate()`, which the parent
45984
+ * constructor calls before this view exists, lowers separately and silently.)
45985
+ */
45986
+ constructor({ logger, ...params }) {
45816
45987
  super(params);
45817
- this.json = OpencodeConfigSchema.parse((0, jsonc_parser.parse)(this.fileContent || "{}"));
45988
+ this.json = OpencodeConfigSchema.parse(this.constructor.lowerConfig((0, jsonc_parser.parse)(this.fileContent || "{}"), logger));
45989
+ }
45990
+ /**
45991
+ * Lowers the OpenCode V2 `mcp` spellings of a parsed config for a layout
45992
+ * that reads them (see {@link lowerOpencodeV2Mcp}), so every reader — import,
45993
+ * the toggle lookup on generate, and the rules feature's `instructions`
45994
+ * write to the same file — accepts a V2-shaped `mcp`. Only the parsed view
45995
+ * is lowered; content written back keeps whatever `mcp` a feature that does
45996
+ * not own the key found there. With a `logger`, each V2 value the lowering
45997
+ * drops is warned about.
45998
+ */
45999
+ static lowerConfig(json, logger) {
46000
+ if (!this.layout.readsV2ConfigSpellings || !isRecord$1(json) || json.mcp === void 0) return json;
46001
+ return {
46002
+ ...json,
46003
+ mcp: lowerOpencodeV2Mcp(json.mcp, (path, reason) => {
46004
+ const quoted = quoteValueForWarning(path);
46005
+ logger?.warn(reason === "conflict" ? `OpenCode MCP: ${quoted} conflicts with a V1 value that takes precedence (the flat "mcp" entry of the same name, or "enabled"), so it is dropped on import.` : `OpenCode MCP: ${quoted} has no Rulesync equivalent, so it is dropped on import.`);
46006
+ })
46007
+ };
45818
46008
  }
45819
46009
  getJson() {
45820
46010
  return this.json;
@@ -45831,7 +46021,7 @@ var OpencodeMcp = class extends ToolMcp {
45831
46021
  relativeFilePath: this.layout.jsonFileName
45832
46022
  };
45833
46023
  }
45834
- static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
46024
+ static async fromFile({ outputRoot = process.cwd(), validate = true, global = false, logger }) {
45835
46025
  const basePaths = this.getSettablePaths({ global });
45836
46026
  const jsonDir = (0, node_path.join)(outputRoot, basePaths.relativeDirPath);
45837
46027
  let fileContent = null;
@@ -45853,7 +46043,8 @@ var OpencodeMcp = class extends ToolMcp {
45853
46043
  relativeDirPath: basePaths.relativeDirPath,
45854
46044
  relativeFilePath,
45855
46045
  fileContent: JSON.stringify(newJson, null, 2),
45856
- validate
46046
+ validate,
46047
+ logger
45857
46048
  });
45858
46049
  }
45859
46050
  static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
@@ -45872,7 +46063,12 @@ var OpencodeMcp = class extends ToolMcp {
45872
46063
  mcpServers: rulesyncMcp.getMcpServers(),
45873
46064
  replacement: "{env:$1}"
45874
46065
  });
45875
- const existingMcp = OpencodeConfigSchema.safeParse((0, jsonc_parser.parse)(fileContent || "{}"));
46066
+ const existingJson = (0, jsonc_parser.parse)(fileContent || "{}");
46067
+ const existingMcp = OpencodeConfigSchema.safeParse(this.lowerConfig(existingJson));
46068
+ if (this.layout.readsV2ConfigSpellings && isRecord$1(existingJson)) {
46069
+ const containers = findOpencodeV2McpContainers(existingJson.mcp);
46070
+ if (containers.length > 0) logger?.warn(`OpenCode MCP: the whole "mcp" key is rewritten in the flat V1 shape, so ${containers.map(quoteValueForWarning).join(" and ")} ${containers.length === 1 ? "is" : "are"} not kept.`);
46071
+ }
45876
46072
  const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
45877
46073
  return new this({
45878
46074
  outputRoot,
@@ -45972,7 +46168,7 @@ var OpencodeMcp = class extends ToolMcp {
45972
46168
  }
45973
46169
  validate() {
45974
46170
  const json = parseJsonc$6(this.fileContent || "{}");
45975
- const result = OpencodeConfigSchema.safeParse(json);
46171
+ const result = OpencodeConfigSchema.safeParse(this.constructor.lowerConfig(json));
45976
46172
  if (!result.success) return {
45977
46173
  success: false,
45978
46174
  error: result.error
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_import = require("./import-Bmb7ofrF.cjs");
2
+ const require_import = require("./import-D_SV_C0k.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.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Rn as ALL_FEATURES, bt as ConsoleLogger, cn as ALL_TOOL_TARGETS, dt as ConfigResolver, i as inspectInputRoots, o as convertFromTool$1, r as generate$1, t as importFromTool$1 } from "./import-DgKWnDni.js";
1
+ import { Rn as ALL_FEATURES, bt as ConsoleLogger, cn as ALL_TOOL_TARGETS, dt as ConfigResolver, i as inspectInputRoots, o as convertFromTool$1, r as generate$1, t as importFromTool$1 } from "./import-5-Uennl1.js";
2
2
  //#region src/index.ts
3
3
  async function generate(options = {}) {
4
4
  const { silent = true, verbose = false, ...rest } = options;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulesync",
3
- "version": "32.0.0",
3
+ "version": "33.0.0",
4
4
  "description": "Unified AI rules management CLI tool that generates configuration files for various AI development tools",
5
5
  "keywords": [
6
6
  "ai",