avrae-ls 0.6.0__py3-none-any.whl → 0.6.2__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.
- avrae_ls/__init__.py +3 -0
- avrae_ls/__main__.py +272 -0
- avrae_ls/alias_preview.py +371 -0
- avrae_ls/alias_tests.py +351 -0
- avrae_ls/api.py +2015 -0
- avrae_ls/argparser.py +430 -0
- avrae_ls/argument_parsing.py +67 -0
- avrae_ls/code_actions.py +282 -0
- avrae_ls/codes.py +3 -0
- avrae_ls/completions.py +1695 -0
- avrae_ls/config.py +480 -0
- avrae_ls/context.py +337 -0
- avrae_ls/cvars.py +115 -0
- avrae_ls/diagnostics.py +826 -0
- avrae_ls/dice.py +33 -0
- avrae_ls/parser.py +68 -0
- avrae_ls/runtime.py +750 -0
- avrae_ls/server.py +447 -0
- avrae_ls/signature_help.py +248 -0
- avrae_ls/symbols.py +274 -0
- {avrae_ls-0.6.0.dist-info → avrae_ls-0.6.2.dist-info}/METADATA +1 -1
- avrae_ls-0.6.2.dist-info/RECORD +34 -0
- draconic/__init__.py +4 -0
- draconic/exceptions.py +157 -0
- draconic/helpers.py +236 -0
- draconic/interpreter.py +1091 -0
- draconic/string.py +100 -0
- draconic/types.py +364 -0
- draconic/utils.py +78 -0
- draconic/versions.py +4 -0
- avrae_ls-0.6.0.dist-info/RECORD +0 -6
- {avrae_ls-0.6.0.dist-info → avrae_ls-0.6.2.dist-info}/WHEEL +0 -0
- {avrae_ls-0.6.0.dist-info → avrae_ls-0.6.2.dist-info}/entry_points.txt +0 -0
- {avrae_ls-0.6.0.dist-info → avrae_ls-0.6.2.dist-info}/licenses/LICENSE +0 -0
avrae_ls/config.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import math
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, Iterable, Mapping, Tuple
|
|
11
|
+
|
|
12
|
+
CONFIG_FILENAME = ".avraels.json"
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
_ENV_VAR_PATTERN = re.compile(r"\$(\w+)|\$\{([^}]+)\}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConfigError(Exception):
|
|
18
|
+
"""Raised when a workspace config file cannot be parsed."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class DiagnosticSettings:
|
|
23
|
+
semantic_level: str = "warning"
|
|
24
|
+
runtime_level: str = "error"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class AvraeServiceConfig:
|
|
29
|
+
base_url: str = "https://api.avrae.io"
|
|
30
|
+
token: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class VarSources:
|
|
35
|
+
cvars: Dict[str, Any] = field(default_factory=dict)
|
|
36
|
+
uvars: Dict[str, Any] = field(default_factory=dict)
|
|
37
|
+
svars: Dict[str, Any] = field(default_factory=dict)
|
|
38
|
+
gvars: Dict[str, Any] = field(default_factory=dict)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def from_data(cls, data: Dict[str, Any] | None) -> "VarSources":
|
|
42
|
+
data = data or {}
|
|
43
|
+
return cls(
|
|
44
|
+
cvars=dict(data.get("cvars") or {}),
|
|
45
|
+
uvars=dict(data.get("uvars") or {}),
|
|
46
|
+
svars=dict(data.get("svars") or {}),
|
|
47
|
+
gvars=dict(data.get("gvars") or {}),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def merge(self, other: "VarSources") -> "VarSources":
|
|
51
|
+
def _merge(lhs: Dict[str, Any], rhs: Dict[str, Any]) -> Dict[str, Any]:
|
|
52
|
+
merged = dict(lhs)
|
|
53
|
+
merged.update(rhs)
|
|
54
|
+
return merged
|
|
55
|
+
|
|
56
|
+
return VarSources(
|
|
57
|
+
cvars=_merge(self.cvars, other.cvars),
|
|
58
|
+
uvars=_merge(self.uvars, other.uvars),
|
|
59
|
+
svars=_merge(self.svars, other.svars),
|
|
60
|
+
gvars=_merge(self.gvars, other.gvars),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def to_initial_names(self) -> Dict[str, Any]:
|
|
64
|
+
names: Dict[str, Any] = {}
|
|
65
|
+
# Bind uvars first so cvars take precedence, matching Avrae's local > cvar > uvar lookup.
|
|
66
|
+
names.update(self.uvars)
|
|
67
|
+
names.update(self.cvars)
|
|
68
|
+
return names
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class ContextProfile:
|
|
73
|
+
name: str
|
|
74
|
+
ctx: Dict[str, Any] = field(default_factory=dict)
|
|
75
|
+
combat: Dict[str, Any] = field(default_factory=dict)
|
|
76
|
+
character: Dict[str, Any] = field(default_factory=dict)
|
|
77
|
+
vars: VarSources = field(default_factory=VarSources)
|
|
78
|
+
description: str = ""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class AvraeLSConfig:
|
|
83
|
+
workspace_root: Path
|
|
84
|
+
enable_gvar_fetch: bool = False
|
|
85
|
+
service: AvraeServiceConfig = field(default_factory=AvraeServiceConfig)
|
|
86
|
+
var_files: Tuple[Path, ...] = field(default_factory=tuple)
|
|
87
|
+
default_profile: str = "default"
|
|
88
|
+
profiles: Dict[str, ContextProfile] = field(default_factory=dict)
|
|
89
|
+
diagnostics: DiagnosticSettings = field(default_factory=DiagnosticSettings)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def default(cls, workspace_root: Path) -> "AvraeLSConfig":
|
|
93
|
+
abilities = {
|
|
94
|
+
"strength": 16,
|
|
95
|
+
"dexterity": 14,
|
|
96
|
+
"constitution": 15,
|
|
97
|
+
"intelligence": 10,
|
|
98
|
+
"wisdom": 12,
|
|
99
|
+
"charisma": 13,
|
|
100
|
+
"prof_bonus": 3,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
def _mod(score: int) -> int:
|
|
104
|
+
return math.floor((score - 10) / 2)
|
|
105
|
+
|
|
106
|
+
skill_profs = {"athletics", "perception", "stealth", "survival"}
|
|
107
|
+
skills = {}
|
|
108
|
+
for name, ability in {
|
|
109
|
+
"acrobatics": "dexterity",
|
|
110
|
+
"animalHandling": "wisdom",
|
|
111
|
+
"arcana": "intelligence",
|
|
112
|
+
"athletics": "strength",
|
|
113
|
+
"deception": "charisma",
|
|
114
|
+
"history": "intelligence",
|
|
115
|
+
"initiative": "dexterity",
|
|
116
|
+
"insight": "wisdom",
|
|
117
|
+
"intimidation": "charisma",
|
|
118
|
+
"investigation": "intelligence",
|
|
119
|
+
"medicine": "wisdom",
|
|
120
|
+
"nature": "intelligence",
|
|
121
|
+
"perception": "wisdom",
|
|
122
|
+
"performance": "charisma",
|
|
123
|
+
"persuasion": "charisma",
|
|
124
|
+
"religion": "intelligence",
|
|
125
|
+
"sleightOfHand": "dexterity",
|
|
126
|
+
"stealth": "dexterity",
|
|
127
|
+
"survival": "wisdom",
|
|
128
|
+
"strength": "strength",
|
|
129
|
+
"dexterity": "dexterity",
|
|
130
|
+
"constitution": "constitution",
|
|
131
|
+
"intelligence": "intelligence",
|
|
132
|
+
"wisdom": "wisdom",
|
|
133
|
+
"charisma": "charisma",
|
|
134
|
+
}.items():
|
|
135
|
+
base = _mod(abilities[ability])
|
|
136
|
+
prof = 1 if name in skill_profs else 0
|
|
137
|
+
skills[name] = {"value": base + abilities["prof_bonus"] * prof, "prof": prof, "bonus": 0, "adv": None}
|
|
138
|
+
|
|
139
|
+
saves = {
|
|
140
|
+
"str": _mod(abilities["strength"]) + abilities["prof_bonus"],
|
|
141
|
+
"dex": _mod(abilities["dexterity"]),
|
|
142
|
+
"con": _mod(abilities["constitution"]) + abilities["prof_bonus"],
|
|
143
|
+
"int": _mod(abilities["intelligence"]),
|
|
144
|
+
"wis": _mod(abilities["wisdom"]),
|
|
145
|
+
"cha": _mod(abilities["charisma"]),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
attacks = [
|
|
149
|
+
{
|
|
150
|
+
"name": "Longsword",
|
|
151
|
+
"verb": "swings",
|
|
152
|
+
"proper": False,
|
|
153
|
+
"activation_type": 1,
|
|
154
|
+
"raw": {"name": "Longsword", "bonus": "+7", "damage": "1d8+4 slashing"},
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "Shortbow",
|
|
158
|
+
"verb": "looses",
|
|
159
|
+
"proper": False,
|
|
160
|
+
"activation_type": 1,
|
|
161
|
+
"raw": {"name": "Shortbow", "bonus": "+6", "damage": "1d6+3 piercing"},
|
|
162
|
+
},
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
spellbook = {
|
|
166
|
+
"dc": 14,
|
|
167
|
+
"sab": 6,
|
|
168
|
+
"caster_level": 5,
|
|
169
|
+
"spell_mod": 3,
|
|
170
|
+
"pact_slot_level": None,
|
|
171
|
+
"num_pact_slots": None,
|
|
172
|
+
"max_pact_slots": None,
|
|
173
|
+
"slots": {1: 4, 2: 2},
|
|
174
|
+
"max_slots": {1: 4, 2: 2},
|
|
175
|
+
"spells": [
|
|
176
|
+
{"name": "Cure Wounds", "dc": None, "sab": None, "mod": None, "prepared": True},
|
|
177
|
+
{"name": "Hunter's Mark", "dc": None, "sab": None, "mod": None, "prepared": True},
|
|
178
|
+
{"name": "Fire Bolt", "dc": None, "sab": None, "mod": None, "prepared": True},
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
consumables = {
|
|
183
|
+
"Hit Dice": {
|
|
184
|
+
"name": "Hit Dice",
|
|
185
|
+
"value": 5,
|
|
186
|
+
"max": 5,
|
|
187
|
+
"min": 0,
|
|
188
|
+
"reset_on": "long",
|
|
189
|
+
"display_type": None,
|
|
190
|
+
"reset_to": None,
|
|
191
|
+
"reset_by": None,
|
|
192
|
+
"title": "d10 hit dice",
|
|
193
|
+
"desc": "Hit dice pool",
|
|
194
|
+
},
|
|
195
|
+
"Bardic Inspiration": {
|
|
196
|
+
"name": "Bardic Inspiration",
|
|
197
|
+
"value": 3,
|
|
198
|
+
"max": 3,
|
|
199
|
+
"min": 0,
|
|
200
|
+
"reset_on": "long",
|
|
201
|
+
"display_type": "bubble",
|
|
202
|
+
"reset_to": "max",
|
|
203
|
+
"reset_by": None,
|
|
204
|
+
"title": None,
|
|
205
|
+
"desc": None,
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
character = {
|
|
210
|
+
"name": "Aelar Wyn",
|
|
211
|
+
"race": "Half-Elf",
|
|
212
|
+
"background": "Outlander",
|
|
213
|
+
"description": "Scout of the Emerald Enclave.",
|
|
214
|
+
"image": "https://example.invalid/aelar.png",
|
|
215
|
+
"owner": 1010101010,
|
|
216
|
+
"upstream": "char_aelar_wyn",
|
|
217
|
+
"sheet_type": "beyond",
|
|
218
|
+
"creature_type": "humanoid",
|
|
219
|
+
"stats": abilities,
|
|
220
|
+
"levels": {"Fighter": 3, "Rogue": 2},
|
|
221
|
+
"attacks": attacks,
|
|
222
|
+
"actions": [
|
|
223
|
+
{
|
|
224
|
+
"name": "Second Wind",
|
|
225
|
+
"activation_type": 3,
|
|
226
|
+
"activation_type_name": "BONUS_ACTION",
|
|
227
|
+
"description": "+1d10+5 hp",
|
|
228
|
+
"snippet": "+1d10+5 hp",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"name": "Action Surge",
|
|
232
|
+
"activation_type": 1,
|
|
233
|
+
"activation_type_name": "ACTION",
|
|
234
|
+
"description": "Take one additional action.",
|
|
235
|
+
"snippet": "Take one additional action.",
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
"skills": skills,
|
|
239
|
+
"saves": saves,
|
|
240
|
+
"resistances": {"resist": [{"dtype": "fire", "unless": [], "only": []}], "vuln": [], "immune": [], "neutral": []},
|
|
241
|
+
"spellbook": spellbook,
|
|
242
|
+
"consumables": consumables,
|
|
243
|
+
"cvars": {"favorite_enemy": "goblinoids", "fighting_style": "defense"},
|
|
244
|
+
"coinpurse": {"pp": 1, "gp": 47, "ep": 0, "sp": 12, "cp": 34},
|
|
245
|
+
"death_saves": {"successes": 0, "fails": 0},
|
|
246
|
+
"max_hp": 44,
|
|
247
|
+
"hp": 38,
|
|
248
|
+
"temp_hp": 3,
|
|
249
|
+
"ac": 17,
|
|
250
|
+
"passive_perception": 15,
|
|
251
|
+
"speed": 30,
|
|
252
|
+
"class_levels": {"Fighter": 3, "Rogue": 2},
|
|
253
|
+
"csettings": {"compact_coins": False},
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
me_combatant = {
|
|
257
|
+
"name": character["name"],
|
|
258
|
+
"id": "cmb_aelar",
|
|
259
|
+
"controller": character["owner"],
|
|
260
|
+
"group": None,
|
|
261
|
+
"race": character["race"],
|
|
262
|
+
"monster_name": None,
|
|
263
|
+
"is_hidden": False,
|
|
264
|
+
"init": 18,
|
|
265
|
+
"initmod": 4,
|
|
266
|
+
"type": "combatant",
|
|
267
|
+
"note": "On watch",
|
|
268
|
+
"effects": [
|
|
269
|
+
{
|
|
270
|
+
"name": "Hunter's Mark",
|
|
271
|
+
"duration": 600,
|
|
272
|
+
"remaining": 540,
|
|
273
|
+
"desc": "Mark one target; deal +1d6 damage to it.",
|
|
274
|
+
"concentration": True,
|
|
275
|
+
"combatant_name": character["name"],
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
"stats": character["stats"],
|
|
279
|
+
"levels": character["levels"],
|
|
280
|
+
"skills": character["skills"],
|
|
281
|
+
"saves": character["saves"],
|
|
282
|
+
"resistances": character["resistances"],
|
|
283
|
+
"spellbook": character["spellbook"],
|
|
284
|
+
"attacks": character["attacks"],
|
|
285
|
+
"max_hp": character["max_hp"],
|
|
286
|
+
"hp": character["hp"],
|
|
287
|
+
"temp_hp": character["temp_hp"],
|
|
288
|
+
"ac": character["ac"],
|
|
289
|
+
"creature_type": character["creature_type"],
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
goblin = {
|
|
293
|
+
"name": "Goblin Cutter",
|
|
294
|
+
"id": "cmb_gob1",
|
|
295
|
+
"controller": None,
|
|
296
|
+
"group": "Goblins",
|
|
297
|
+
"race": None,
|
|
298
|
+
"monster_name": "Goblin",
|
|
299
|
+
"is_hidden": False,
|
|
300
|
+
"init": 12,
|
|
301
|
+
"initmod": 2,
|
|
302
|
+
"type": "combatant",
|
|
303
|
+
"note": "",
|
|
304
|
+
"effects": [],
|
|
305
|
+
"stats": {"strength": 8, "dexterity": 14, "constitution": 10, "intelligence": 8, "wisdom": 10, "charisma": 8, "prof_bonus": 2},
|
|
306
|
+
"levels": {},
|
|
307
|
+
"skills": {"stealth": {"value": 6, "prof": 1, "bonus": 0, "adv": None}},
|
|
308
|
+
"saves": {"dex": 4},
|
|
309
|
+
"resistances": {"resist": [], "vuln": [], "immune": [], "neutral": []},
|
|
310
|
+
"spellbook": {"spells": []},
|
|
311
|
+
"attacks": [{"name": "Scimitar", "verb": "slashes", "proper": False, "activation_type": 1, "raw": {"name": "Scimitar", "bonus": "+4", "damage": "1d6+2 slashing"}}],
|
|
312
|
+
"max_hp": 11,
|
|
313
|
+
"hp": 11,
|
|
314
|
+
"temp_hp": 0,
|
|
315
|
+
"ac": 15,
|
|
316
|
+
"creature_type": "humanoid",
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
group = {"name": "Goblins", "id": "grp_goblins", "init": 12, "type": "group", "combatants": [goblin]}
|
|
320
|
+
|
|
321
|
+
combat = {
|
|
322
|
+
"name": "Goblin Ambush",
|
|
323
|
+
"round_num": 2,
|
|
324
|
+
"turn_num": 15,
|
|
325
|
+
"combatants": [me_combatant, goblin],
|
|
326
|
+
"groups": [group],
|
|
327
|
+
"me": me_combatant,
|
|
328
|
+
"current": group,
|
|
329
|
+
"metadata": {"scene": "Forest road", "weather": "light rain"},
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
ctx = {
|
|
333
|
+
"guild": {"id": 123456789012345678, "name": "Fabled Realms"},
|
|
334
|
+
"channel": {
|
|
335
|
+
"id": 234567890123456789,
|
|
336
|
+
"name": "tavern-rp",
|
|
337
|
+
"topic": "Adventuring party chat",
|
|
338
|
+
"category": {"id": 9876543210, "name": "Adventures"},
|
|
339
|
+
"parent": None,
|
|
340
|
+
},
|
|
341
|
+
"author": {
|
|
342
|
+
"id": 345678901234567890,
|
|
343
|
+
"name": "AelarW",
|
|
344
|
+
"discriminator": "0420",
|
|
345
|
+
"display_name": "Aelar Wyn",
|
|
346
|
+
"roles": [{"id": 4567, "name": "DM"}, {"id": 4568, "name": "Player"}],
|
|
347
|
+
},
|
|
348
|
+
"prefix": "!",
|
|
349
|
+
"alias": "mockalias",
|
|
350
|
+
"message_id": 456789012345678901,
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
profile = ContextProfile(
|
|
354
|
+
name="default",
|
|
355
|
+
ctx=ctx,
|
|
356
|
+
combat=combat,
|
|
357
|
+
character=character,
|
|
358
|
+
description="Built-in Avrae LS mock profile with realistic sample data.",
|
|
359
|
+
)
|
|
360
|
+
return cls(
|
|
361
|
+
workspace_root=workspace_root,
|
|
362
|
+
profiles={"default": profile},
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _expand_env_vars(data: Any, env: Mapping[str, str], missing_vars: set[str]) -> Any:
|
|
367
|
+
if isinstance(data, dict):
|
|
368
|
+
return {key: _expand_env_vars(value, env, missing_vars) for key, value in data.items()}
|
|
369
|
+
if isinstance(data, list):
|
|
370
|
+
return [_expand_env_vars(value, env, missing_vars) for value in data]
|
|
371
|
+
if isinstance(data, str):
|
|
372
|
+
def _replace(match: re.Match[str]) -> str:
|
|
373
|
+
var = match.group(1) or match.group(2) or ""
|
|
374
|
+
if var in env:
|
|
375
|
+
return env[var]
|
|
376
|
+
missing_vars.add(var)
|
|
377
|
+
return ""
|
|
378
|
+
|
|
379
|
+
return _ENV_VAR_PATTERN.sub(_replace, data)
|
|
380
|
+
return data
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _coerce_optional_str(value: Any) -> str | None:
|
|
384
|
+
if value is None:
|
|
385
|
+
return None
|
|
386
|
+
value_str = str(value)
|
|
387
|
+
return value_str if value_str.strip() else None
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def load_config(workspace_root: Path, *, default_enable_gvar_fetch: bool = False) -> Tuple[AvraeLSConfig, Iterable[str]]:
|
|
391
|
+
"""Load `.avraels.json` from the workspace root, returning config and warnings."""
|
|
392
|
+
path = workspace_root / CONFIG_FILENAME
|
|
393
|
+
if not path.exists():
|
|
394
|
+
cfg = AvraeLSConfig.default(workspace_root)
|
|
395
|
+
cfg.enable_gvar_fetch = default_enable_gvar_fetch
|
|
396
|
+
env_token = _coerce_optional_str(os.environ.get("AVRAE_TOKEN"))
|
|
397
|
+
if env_token:
|
|
398
|
+
cfg.service.token = env_token
|
|
399
|
+
return cfg, []
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
raw = json.loads(path.read_text())
|
|
403
|
+
except json.JSONDecodeError as exc:
|
|
404
|
+
warning = f"Failed to parse {CONFIG_FILENAME}: {exc}"
|
|
405
|
+
log.warning(warning)
|
|
406
|
+
return AvraeLSConfig.default(workspace_root), [warning]
|
|
407
|
+
|
|
408
|
+
warnings: list[str] = []
|
|
409
|
+
env_missing: set[str] = set()
|
|
410
|
+
env = dict(os.environ)
|
|
411
|
+
env.setdefault("workspaceRoot", str(workspace_root))
|
|
412
|
+
env.setdefault("workspaceFolder", str(workspace_root))
|
|
413
|
+
raw = _expand_env_vars(raw, env, env_missing)
|
|
414
|
+
for var in sorted(env_missing):
|
|
415
|
+
warning = f"{CONFIG_FILENAME}: environment variable '{var}' is not set; substituting an empty string."
|
|
416
|
+
warnings.append(warning)
|
|
417
|
+
log.warning(warning)
|
|
418
|
+
|
|
419
|
+
enable_gvar_fetch = bool(raw.get("enableGvarFetch", default_enable_gvar_fetch))
|
|
420
|
+
|
|
421
|
+
service_cfg = raw.get("avraeService") or {}
|
|
422
|
+
env_token = _coerce_optional_str(env.get("AVRAE_TOKEN"))
|
|
423
|
+
service = AvraeServiceConfig(
|
|
424
|
+
base_url=str(service_cfg.get("baseUrl") or AvraeServiceConfig.base_url),
|
|
425
|
+
token=_coerce_optional_str(service_cfg.get("token")) or env_token,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
diag_cfg = raw.get("diagnostics") or {}
|
|
429
|
+
diagnostics = DiagnosticSettings(
|
|
430
|
+
semantic_level=str(diag_cfg.get("semanticLevel") or "warning").lower(),
|
|
431
|
+
runtime_level=str(diag_cfg.get("runtimeLevel") or "error").lower(),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
var_files = tuple(
|
|
435
|
+
_resolve_var_file(workspace_root, file_path)
|
|
436
|
+
for file_path in raw.get("varFiles", [])
|
|
437
|
+
if isinstance(file_path, str)
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
profiles: Dict[str, ContextProfile] = {}
|
|
441
|
+
raw_profiles = raw.get("profiles") or {}
|
|
442
|
+
for name, data in raw_profiles.items():
|
|
443
|
+
profiles[name] = ContextProfile(
|
|
444
|
+
name=name,
|
|
445
|
+
ctx=dict(data.get("ctx") or {}),
|
|
446
|
+
combat=dict(data.get("combat") or {}),
|
|
447
|
+
character=dict(data.get("character") or {}),
|
|
448
|
+
vars=VarSources.from_data(data.get("vars")),
|
|
449
|
+
description=str(data.get("description") or ""),
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
default_profile = str(raw.get("defaultProfile") or "default")
|
|
453
|
+
if default_profile not in profiles and profiles:
|
|
454
|
+
warnings.append(
|
|
455
|
+
f"defaultProfile '{default_profile}' not found; falling back to first profile in file."
|
|
456
|
+
)
|
|
457
|
+
default_profile = next(iter(profiles))
|
|
458
|
+
|
|
459
|
+
if not profiles:
|
|
460
|
+
base = AvraeLSConfig.default(workspace_root)
|
|
461
|
+
profiles = base.profiles
|
|
462
|
+
default_profile = base.default_profile
|
|
463
|
+
|
|
464
|
+
cfg = AvraeLSConfig(
|
|
465
|
+
workspace_root=workspace_root,
|
|
466
|
+
enable_gvar_fetch=enable_gvar_fetch,
|
|
467
|
+
service=service,
|
|
468
|
+
var_files=var_files,
|
|
469
|
+
default_profile=default_profile,
|
|
470
|
+
profiles=profiles,
|
|
471
|
+
diagnostics=diagnostics,
|
|
472
|
+
)
|
|
473
|
+
return cfg, warnings
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _resolve_var_file(root: Path, file_path: str) -> Path:
|
|
477
|
+
candidate = Path(file_path)
|
|
478
|
+
if not candidate.is_absolute():
|
|
479
|
+
candidate = root / candidate
|
|
480
|
+
return candidate
|