ai-agent-rules 0.11.0__py3-none-any.whl → 0.15.8__py3-none-any.whl
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.
Potentially problematic release.
This version of ai-agent-rules might be problematic. Click here for more details.
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/METADATA +91 -6
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/RECORD +27 -16
- ai_rules/agents/claude.py +3 -1
- ai_rules/agents/cursor.py +70 -0
- ai_rules/agents/goose.py +4 -1
- ai_rules/bootstrap/__init__.py +8 -4
- ai_rules/bootstrap/installer.py +95 -23
- ai_rules/bootstrap/updater.py +183 -43
- ai_rules/cli.py +360 -42
- ai_rules/config/AGENTS.md +5 -4
- ai_rules/config/claude/CLAUDE.md +1 -0
- ai_rules/config/claude/commands/agents-md.md +422 -0
- ai_rules/config/claude/settings.json +7 -4
- ai_rules/config/cursor/keybindings.json +14 -0
- ai_rules/config/cursor/settings.json +81 -0
- ai_rules/config/goose/.goosehints +1 -0
- ai_rules/config/profiles/default.yaml +6 -0
- ai_rules/config/profiles/work.yaml +11 -0
- ai_rules/config.py +55 -46
- ai_rules/mcp.py +2 -3
- ai_rules/profiles.py +187 -0
- ai_rules/state.py +47 -0
- ai_rules/utils.py +35 -0
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/WHEEL +0 -0
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/entry_points.txt +0 -0
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/licenses/LICENSE +0 -0
- {ai_agent_rules-0.11.0.dist-info → ai_agent_rules-0.15.8.dist-info}/top_level.txt +0 -0
ai_rules/config.py
CHANGED
|
@@ -12,6 +12,8 @@ from typing import Any
|
|
|
12
12
|
|
|
13
13
|
import yaml
|
|
14
14
|
|
|
15
|
+
from ai_rules.utils import deep_merge
|
|
16
|
+
|
|
15
17
|
__all__ = [
|
|
16
18
|
"Config",
|
|
17
19
|
"AGENT_CONFIG_METADATA",
|
|
@@ -25,6 +27,10 @@ AGENT_CONFIG_METADATA = {
|
|
|
25
27
|
"config_file": "settings.json",
|
|
26
28
|
"format": "json",
|
|
27
29
|
},
|
|
30
|
+
"cursor": {
|
|
31
|
+
"config_file": "settings.json",
|
|
32
|
+
"format": "json",
|
|
33
|
+
},
|
|
28
34
|
"goose": {
|
|
29
35
|
"config_file": "config.yaml",
|
|
30
36
|
"format": "yaml",
|
|
@@ -244,40 +250,65 @@ class Config:
|
|
|
244
250
|
exclude_symlinks: list[str] | None = None,
|
|
245
251
|
settings_overrides: dict[str, dict[str, Any]] | None = None,
|
|
246
252
|
mcp_overrides: dict[str, dict[str, Any]] | None = None,
|
|
253
|
+
profile_name: str | None = None,
|
|
247
254
|
):
|
|
248
255
|
self.exclude_symlinks = set(exclude_symlinks or [])
|
|
249
256
|
self.settings_overrides = settings_overrides or {}
|
|
250
257
|
self.mcp_overrides = mcp_overrides or {}
|
|
258
|
+
self.profile_name = profile_name
|
|
251
259
|
|
|
252
260
|
@classmethod
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
"""Load configuration from ~/.ai-rules-config.yaml.
|
|
261
|
+
def load(cls, profile: str | None = None) -> "Config":
|
|
262
|
+
"""Load configuration from profile and ~/.ai-rules-config.yaml.
|
|
256
263
|
|
|
257
|
-
|
|
264
|
+
Merge order (lowest to highest priority):
|
|
265
|
+
1. Profile overrides (if profile specified, defaults to "default")
|
|
266
|
+
2. Local overrides from ~/.ai-rules-config.yaml
|
|
258
267
|
|
|
259
|
-
|
|
268
|
+
Args:
|
|
269
|
+
profile: Optional profile name to load (default: "default")
|
|
260
270
|
|
|
261
|
-
|
|
262
|
-
|
|
271
|
+
Returns:
|
|
272
|
+
Config instance with merged overrides
|
|
263
273
|
"""
|
|
264
|
-
|
|
274
|
+
return cls._load_cached(profile or "default")
|
|
265
275
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
276
|
+
@classmethod
|
|
277
|
+
@lru_cache(maxsize=8)
|
|
278
|
+
def _load_cached(cls, profile_name: str) -> "Config":
|
|
279
|
+
"""Internal cached loader keyed by profile name."""
|
|
280
|
+
from ai_rules.profiles import ProfileLoader, ProfileNotFoundError
|
|
281
|
+
|
|
282
|
+
loader = ProfileLoader()
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
profile_data = loader.load_profile(profile_name)
|
|
286
|
+
except ProfileNotFoundError:
|
|
287
|
+
raise
|
|
288
|
+
|
|
289
|
+
exclude_symlinks = list(profile_data.exclude_symlinks)
|
|
290
|
+
settings_overrides = copy.deepcopy(profile_data.settings_overrides)
|
|
291
|
+
mcp_overrides = copy.deepcopy(profile_data.mcp_overrides)
|
|
269
292
|
|
|
293
|
+
user_config_path = Path.home() / ".ai-rules-config.yaml"
|
|
270
294
|
if user_config_path.exists():
|
|
271
295
|
with open(user_config_path) as f:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
296
|
+
user_data = yaml.safe_load(f) or {}
|
|
297
|
+
|
|
298
|
+
user_excludes = user_data.get("exclude_symlinks", [])
|
|
299
|
+
exclude_symlinks = list(set(exclude_symlinks) | set(user_excludes))
|
|
300
|
+
|
|
301
|
+
user_settings = user_data.get("settings_overrides", {})
|
|
302
|
+
settings_overrides = deep_merge(settings_overrides, user_settings)
|
|
303
|
+
|
|
304
|
+
user_mcp = user_data.get("mcp_overrides", {})
|
|
305
|
+
mcp_overrides = deep_merge(mcp_overrides, user_mcp)
|
|
276
306
|
|
|
277
307
|
return cls(
|
|
278
308
|
exclude_symlinks=exclude_symlinks,
|
|
279
309
|
settings_overrides=settings_overrides,
|
|
280
310
|
mcp_overrides=mcp_overrides,
|
|
311
|
+
profile_name=profile_name,
|
|
281
312
|
)
|
|
282
313
|
|
|
283
314
|
def is_excluded(self, symlink_target: str) -> bool:
|
|
@@ -301,36 +332,6 @@ class Config:
|
|
|
301
332
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
302
333
|
return cache_dir
|
|
303
334
|
|
|
304
|
-
@staticmethod
|
|
305
|
-
def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
306
|
-
"""Deep merge two dictionaries, with override values taking precedence.
|
|
307
|
-
|
|
308
|
-
Supports merging nested dictionaries and arrays. Arrays are merged element-by-element,
|
|
309
|
-
with dict elements being recursively merged.
|
|
310
|
-
|
|
311
|
-
Uses deep copy to prevent mutation of the base dictionary.
|
|
312
|
-
"""
|
|
313
|
-
result = copy.deepcopy(base)
|
|
314
|
-
for key, value in override.items():
|
|
315
|
-
if key not in result:
|
|
316
|
-
result[key] = value
|
|
317
|
-
elif isinstance(result[key], dict) and isinstance(value, dict):
|
|
318
|
-
result[key] = Config._deep_merge(result[key], value)
|
|
319
|
-
elif isinstance(result[key], list) and isinstance(value, list):
|
|
320
|
-
merged_array = copy.deepcopy(result[key])
|
|
321
|
-
for i, item in enumerate(value):
|
|
322
|
-
if i < len(merged_array):
|
|
323
|
-
if isinstance(merged_array[i], dict) and isinstance(item, dict):
|
|
324
|
-
merged_array[i] = Config._deep_merge(merged_array[i], item)
|
|
325
|
-
else:
|
|
326
|
-
merged_array[i] = item
|
|
327
|
-
else:
|
|
328
|
-
merged_array.append(item)
|
|
329
|
-
result[key] = merged_array
|
|
330
|
-
else:
|
|
331
|
-
result[key] = value
|
|
332
|
-
return result
|
|
333
|
-
|
|
334
335
|
def merge_settings(
|
|
335
336
|
self, agent: str, base_settings: dict[str, Any]
|
|
336
337
|
) -> dict[str, Any]:
|
|
@@ -346,7 +347,7 @@ class Config:
|
|
|
346
347
|
if agent not in self.settings_overrides:
|
|
347
348
|
return base_settings
|
|
348
349
|
|
|
349
|
-
return
|
|
350
|
+
return deep_merge(base_settings, self.settings_overrides[agent])
|
|
350
351
|
|
|
351
352
|
def get_merged_settings_path(self, agent: str) -> Path | None:
|
|
352
353
|
"""Get the path to cached merged settings for an agent.
|
|
@@ -429,6 +430,14 @@ class Config:
|
|
|
429
430
|
if user_config_path.stat().st_mtime > cache_mtime:
|
|
430
431
|
return True
|
|
431
432
|
|
|
433
|
+
if self.profile_name and self.profile_name != "default":
|
|
434
|
+
from ai_rules.profiles import ProfileLoader
|
|
435
|
+
|
|
436
|
+
loader = ProfileLoader()
|
|
437
|
+
profile_path = loader._profiles_dir / f"{self.profile_name}.yaml"
|
|
438
|
+
if profile_path.exists() and profile_path.stat().st_mtime > cache_mtime:
|
|
439
|
+
return True
|
|
440
|
+
|
|
432
441
|
return False
|
|
433
442
|
|
|
434
443
|
def get_cache_diff(self, agent: str, base_settings_path: Path) -> str | None:
|
ai_rules/mcp.py
CHANGED
|
@@ -12,6 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Any, cast
|
|
13
13
|
|
|
14
14
|
from .config import Config
|
|
15
|
+
from .utils import deep_merge
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class OperationResult(Enum):
|
|
@@ -66,9 +67,7 @@ class MCPManager:
|
|
|
66
67
|
merged_mcps = {}
|
|
67
68
|
for name, _mcp_config in {**base_mcps, **mcp_overrides}.items():
|
|
68
69
|
if name in base_mcps and name in mcp_overrides:
|
|
69
|
-
merged_mcps[name] =
|
|
70
|
-
base_mcps[name], mcp_overrides[name]
|
|
71
|
-
)
|
|
70
|
+
merged_mcps[name] = deep_merge(base_mcps[name], mcp_overrides[name])
|
|
72
71
|
elif name in base_mcps:
|
|
73
72
|
merged_mcps[name] = copy.deepcopy(base_mcps[name])
|
|
74
73
|
else:
|
ai_rules/profiles.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Profile loading and inheritance resolution."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from importlib.resources import files as resource_files
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from ai_rules.utils import deep_merge
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Profile:
|
|
15
|
+
"""A named collection of configuration overrides."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
description: str = ""
|
|
19
|
+
extends: str | None = None
|
|
20
|
+
settings_overrides: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
21
|
+
exclude_symlinks: list[str] = field(default_factory=list)
|
|
22
|
+
mcp_overrides: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ProfileError(Exception):
|
|
26
|
+
"""Base exception for profile-related errors."""
|
|
27
|
+
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ProfileNotFoundError(ProfileError):
|
|
32
|
+
"""Raised when a profile is not found."""
|
|
33
|
+
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CircularInheritanceError(ProfileError):
|
|
38
|
+
"""Raised when circular profile inheritance is detected."""
|
|
39
|
+
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ProfileLoader:
|
|
44
|
+
"""Loads and resolves profile inheritance."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, profiles_dir: Path | None = None):
|
|
47
|
+
"""Initialize profile loader.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
profiles_dir: Optional override for profiles directory (for testing)
|
|
51
|
+
"""
|
|
52
|
+
if profiles_dir:
|
|
53
|
+
self._profiles_dir = profiles_dir
|
|
54
|
+
else:
|
|
55
|
+
config_resource = resource_files("ai_rules") / "config" / "profiles"
|
|
56
|
+
self._profiles_dir = Path(str(config_resource))
|
|
57
|
+
|
|
58
|
+
def list_profiles(self) -> list[str]:
|
|
59
|
+
"""List all available profile names."""
|
|
60
|
+
if not self._profiles_dir.exists():
|
|
61
|
+
return ["default"]
|
|
62
|
+
|
|
63
|
+
profiles = []
|
|
64
|
+
for path in self._profiles_dir.glob("*.yaml"):
|
|
65
|
+
profiles.append(path.stem)
|
|
66
|
+
|
|
67
|
+
if "default" not in profiles:
|
|
68
|
+
profiles.append("default")
|
|
69
|
+
|
|
70
|
+
return sorted(profiles)
|
|
71
|
+
|
|
72
|
+
def load_profile(self, name: str) -> Profile:
|
|
73
|
+
"""Load a profile by name, resolving inheritance.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
name: Profile name (without .yaml extension)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Fully resolved Profile with inherited values merged
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ProfileNotFoundError: If profile doesn't exist
|
|
83
|
+
CircularInheritanceError: If circular inheritance detected
|
|
84
|
+
"""
|
|
85
|
+
return self._load_with_inheritance(name, visited=set())
|
|
86
|
+
|
|
87
|
+
def _load_with_inheritance(self, name: str, visited: set[str]) -> Profile:
|
|
88
|
+
"""Recursively load profile with inheritance chain."""
|
|
89
|
+
if name in visited:
|
|
90
|
+
cycle = " -> ".join(visited) + f" -> {name}"
|
|
91
|
+
raise CircularInheritanceError(
|
|
92
|
+
f"Circular profile inheritance detected: {cycle}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
visited.add(name)
|
|
96
|
+
|
|
97
|
+
profile_path = self._profiles_dir / f"{name}.yaml"
|
|
98
|
+
|
|
99
|
+
if not profile_path.exists():
|
|
100
|
+
if name == "default":
|
|
101
|
+
return Profile(
|
|
102
|
+
name="default", description="Default profile (no overrides)"
|
|
103
|
+
)
|
|
104
|
+
available = self.list_profiles()
|
|
105
|
+
raise ProfileNotFoundError(
|
|
106
|
+
f"Profile '{name}' not found. Available profiles: {', '.join(available)}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
with open(profile_path) as f:
|
|
111
|
+
data = yaml.safe_load(f) or {}
|
|
112
|
+
except yaml.YAMLError as e:
|
|
113
|
+
raise ProfileError(f"Profile '{name}' has invalid YAML: {e}") from e
|
|
114
|
+
|
|
115
|
+
self._validate_profile_data(data, name)
|
|
116
|
+
|
|
117
|
+
profile = Profile(
|
|
118
|
+
name=data.get("name", name),
|
|
119
|
+
description=data.get("description", ""),
|
|
120
|
+
extends=data.get("extends"),
|
|
121
|
+
settings_overrides=data.get("settings_overrides", {}),
|
|
122
|
+
exclude_symlinks=data.get("exclude_symlinks", []),
|
|
123
|
+
mcp_overrides=data.get("mcp_overrides", {}),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if profile.extends:
|
|
127
|
+
parent = self._load_with_inheritance(profile.extends, visited.copy())
|
|
128
|
+
profile = self._merge_profiles(parent, profile)
|
|
129
|
+
|
|
130
|
+
return profile
|
|
131
|
+
|
|
132
|
+
def _validate_profile_data(self, data: dict[str, Any], profile_name: str) -> None:
|
|
133
|
+
"""Validate profile data types."""
|
|
134
|
+
if "settings_overrides" in data and not isinstance(
|
|
135
|
+
data["settings_overrides"], dict
|
|
136
|
+
):
|
|
137
|
+
raise ProfileError(
|
|
138
|
+
f"Profile '{profile_name}': settings_overrides must be a dict"
|
|
139
|
+
)
|
|
140
|
+
if "exclude_symlinks" in data and not isinstance(
|
|
141
|
+
data["exclude_symlinks"], list
|
|
142
|
+
):
|
|
143
|
+
raise ProfileError(
|
|
144
|
+
f"Profile '{profile_name}': exclude_symlinks must be a list"
|
|
145
|
+
)
|
|
146
|
+
if "mcp_overrides" in data and not isinstance(data["mcp_overrides"], dict):
|
|
147
|
+
raise ProfileError(
|
|
148
|
+
f"Profile '{profile_name}': mcp_overrides must be a dict"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _merge_profiles(self, parent: Profile, child: Profile) -> Profile:
|
|
152
|
+
"""Merge parent profile into child, with child taking precedence."""
|
|
153
|
+
merged_settings = deep_merge(
|
|
154
|
+
parent.settings_overrides, child.settings_overrides
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
merged_mcp = deep_merge(parent.mcp_overrides, child.mcp_overrides)
|
|
158
|
+
|
|
159
|
+
merged_excludes = list(
|
|
160
|
+
set(parent.exclude_symlinks) | set(child.exclude_symlinks)
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return Profile(
|
|
164
|
+
name=child.name,
|
|
165
|
+
description=child.description,
|
|
166
|
+
extends=child.extends,
|
|
167
|
+
settings_overrides=merged_settings,
|
|
168
|
+
exclude_symlinks=merged_excludes,
|
|
169
|
+
mcp_overrides=merged_mcp,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def get_profile_info(self, name: str) -> dict[str, Any]:
|
|
173
|
+
"""Get profile information without resolving inheritance."""
|
|
174
|
+
profile_path = self._profiles_dir / f"{name}.yaml"
|
|
175
|
+
if not profile_path.exists():
|
|
176
|
+
if name == "default":
|
|
177
|
+
return {
|
|
178
|
+
"name": "default",
|
|
179
|
+
"description": "Default profile (no overrides)",
|
|
180
|
+
}
|
|
181
|
+
raise ProfileNotFoundError(f"Profile '{name}' not found")
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
with open(profile_path) as f:
|
|
185
|
+
return yaml.safe_load(f) or {}
|
|
186
|
+
except yaml.YAMLError as e:
|
|
187
|
+
raise ProfileError(f"Profile '{name}' has invalid YAML: {e}") from e
|
ai_rules/state.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""State management for ai-rules."""
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _get_state_file() -> Path:
|
|
11
|
+
"""Get the state file path."""
|
|
12
|
+
return Path.home() / ".ai-rules" / "state.yaml"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_state() -> dict[str, Any]:
|
|
16
|
+
"""Load state from file."""
|
|
17
|
+
state_file = _get_state_file()
|
|
18
|
+
if not state_file.exists():
|
|
19
|
+
return {}
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
with state_file.open() as f:
|
|
23
|
+
return yaml.safe_load(f) or {}
|
|
24
|
+
except Exception:
|
|
25
|
+
return {}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _save_state(state: dict[str, Any]) -> None:
|
|
29
|
+
"""Save state to file."""
|
|
30
|
+
state_file = _get_state_file()
|
|
31
|
+
state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
with state_file.open("w") as f:
|
|
33
|
+
yaml.dump(state, f, default_flow_style=False, sort_keys=False)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_active_profile() -> str | None:
|
|
37
|
+
"""Get the currently active profile."""
|
|
38
|
+
state = get_state()
|
|
39
|
+
return state.get("active_profile")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def set_active_profile(profile: str) -> None:
|
|
43
|
+
"""Set the active profile."""
|
|
44
|
+
state = get_state()
|
|
45
|
+
state["active_profile"] = profile
|
|
46
|
+
state["last_install"] = datetime.now(UTC).isoformat()
|
|
47
|
+
_save_state(state)
|
ai_rules/utils.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Shared utility functions."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
9
|
+
"""Deep merge two dictionaries, with override values taking precedence.
|
|
10
|
+
|
|
11
|
+
Supports merging nested dictionaries and arrays. Arrays are merged element-by-element,
|
|
12
|
+
with dict elements being recursively merged.
|
|
13
|
+
|
|
14
|
+
Uses deep copy to prevent mutation of the base dictionary.
|
|
15
|
+
"""
|
|
16
|
+
result = copy.deepcopy(base)
|
|
17
|
+
for key, value in override.items():
|
|
18
|
+
if key not in result:
|
|
19
|
+
result[key] = value
|
|
20
|
+
elif isinstance(result[key], dict) and isinstance(value, dict):
|
|
21
|
+
result[key] = deep_merge(result[key], value)
|
|
22
|
+
elif isinstance(result[key], list) and isinstance(value, list):
|
|
23
|
+
merged_array = copy.deepcopy(result[key])
|
|
24
|
+
for i, item in enumerate(value):
|
|
25
|
+
if i < len(merged_array):
|
|
26
|
+
if isinstance(merged_array[i], dict) and isinstance(item, dict):
|
|
27
|
+
merged_array[i] = deep_merge(merged_array[i], item)
|
|
28
|
+
else:
|
|
29
|
+
merged_array[i] = item
|
|
30
|
+
else:
|
|
31
|
+
merged_array.append(item)
|
|
32
|
+
result[key] = merged_array
|
|
33
|
+
else:
|
|
34
|
+
result[key] = value
|
|
35
|
+
return result
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|