oasr 0.5.0__py3-none-any.whl → 0.5.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.
config/__init__.py CHANGED
@@ -12,6 +12,7 @@ else:
12
12
  import tomli_w
13
13
 
14
14
  from config.defaults import DEFAULT_CONFIG
15
+ from config.env import load_env_config, merge_configs
15
16
  from config.schema import validate_config
16
17
 
17
18
  OASR_DIR = Path.home() / ".oasr"
@@ -40,19 +41,27 @@ def ensure_skills_dir() -> Path:
40
41
  return ensure_oasr_dir()
41
42
 
42
43
 
43
- def load_config(config_path: Path | None = None) -> dict[str, Any]:
44
- """Load configuration from TOML file.
44
+ def load_config(config_path: Path | None = None, cli_overrides: dict[str, Any] | None = None) -> dict[str, Any]:
45
+ """Load configuration from multiple sources with precedence.
46
+
47
+ Precedence order (highest to lowest):
48
+ 1. cli_overrides - explicit CLI flags
49
+ 2. Environment variables (OASR_*)
50
+ 3. Config file (~/.oasr/config.toml)
51
+ 4. Built-in defaults
45
52
 
46
53
  Args:
47
54
  config_path: Override config file path. Defaults to ~/.oasr/config.toml.
55
+ cli_overrides: Optional CLI flag overrides (highest precedence)
48
56
 
49
57
  Returns:
50
- Configuration dictionary with defaults applied.
58
+ Merged configuration dictionary with all sources applied.
51
59
  """
52
60
  path = config_path or CONFIG_FILE
61
+ cli_overrides = cli_overrides or {}
53
62
 
54
63
  # Deep copy defaults
55
- config = {
64
+ defaults = {
56
65
  "validation": DEFAULT_CONFIG["validation"].copy(),
57
66
  "adapter": DEFAULT_CONFIG["adapter"].copy(),
58
67
  "agent": DEFAULT_CONFIG["agent"].copy(),
@@ -60,22 +69,22 @@ def load_config(config_path: Path | None = None) -> dict[str, Any]:
60
69
  "profiles": {k: v.copy() for k, v in DEFAULT_CONFIG["profiles"].items()},
61
70
  }
62
71
 
72
+ # Load config file
73
+ file_config = {}
63
74
  if path.exists():
64
75
  with open(path, "rb") as f:
65
- loaded = tomllib.load(f)
66
-
67
- if "validation" in loaded:
68
- config["validation"].update(loaded["validation"])
69
- if "adapter" in loaded:
70
- config["adapter"].update(loaded["adapter"])
71
- if "agent" in loaded:
72
- config["agent"].update(loaded["agent"])
73
- if "oasr" in loaded:
74
- config["oasr"].update(loaded["oasr"])
75
- if "profiles" in loaded:
76
- # Merge user profiles with defaults (user profiles take precedence)
77
- for profile_name, profile_data in loaded["profiles"].items():
78
- config["profiles"][profile_name] = profile_data
76
+ file_config = tomllib.load(f)
77
+
78
+ # Load environment variables
79
+ env_config = load_env_config()
80
+
81
+ # Merge all sources with precedence
82
+ config = merge_configs(
83
+ cli_overrides=cli_overrides,
84
+ env_config=env_config,
85
+ file_config=file_config,
86
+ defaults=defaults,
87
+ )
79
88
 
80
89
  return config
81
90
 
config/env.py ADDED
@@ -0,0 +1,248 @@
1
+ """Environment variable configuration support.
2
+
3
+ This module provides functionality to load OASR configuration from environment
4
+ variables, following the pattern: OASR_<SECTION>_<KEY>
5
+
6
+ Precedence order:
7
+ 1. CLI flags (highest)
8
+ 2. Environment variables
9
+ 3. Config file
10
+ 4. Built-in defaults (lowest)
11
+
12
+ Environment variable naming:
13
+ - Prefix: OASR_
14
+ - Format: OASR_<SECTION>_<KEY> (uppercase, underscore-separated)
15
+ - Examples:
16
+ OASR_AGENT=codex → agent.default = "codex"
17
+ OASR_PROFILE=dev → oasr.default_profile = "dev"
18
+ OASR_VALIDATION_STRICT=true → validation.strict = true
19
+
20
+ Type handling:
21
+ - Strings: as-is
22
+ - Booleans: true/false, 1/0, yes/no, on/off (case-insensitive)
23
+ - Integers: parsed with int()
24
+ - Lists: comma-separated values
25
+ """
26
+
27
+ import os
28
+ from typing import Any
29
+
30
+ # Mapping of environment variable names to config paths
31
+ ENV_VAR_MAP = {
32
+ "OASR_AGENT": ("agent", "default"),
33
+ "OASR_PROFILE": ("oasr", "default_profile"),
34
+ "OASR_VALIDATION_STRICT": ("validation", "strict"),
35
+ "OASR_VALIDATION_MAX_LINES": ("validation", "reference_max_lines"),
36
+ "OASR_ADAPTER_TARGETS": ("adapter", "default_targets"),
37
+ }
38
+
39
+
40
+ def parse_bool(value: str) -> bool:
41
+ """Parse boolean from string.
42
+
43
+ Args:
44
+ value: String value to parse
45
+
46
+ Returns:
47
+ Boolean value
48
+
49
+ Raises:
50
+ ValueError: If value cannot be parsed as boolean
51
+ """
52
+ value_lower = value.lower()
53
+ if value_lower in ("true", "1", "yes", "on"):
54
+ return True
55
+ if value_lower in ("false", "0", "no", "off"):
56
+ return False
57
+ raise ValueError(f"Cannot parse '{value}' as boolean")
58
+
59
+
60
+ def parse_int(value: str) -> int:
61
+ """Parse integer from string.
62
+
63
+ Args:
64
+ value: String value to parse
65
+
66
+ Returns:
67
+ Integer value
68
+
69
+ Raises:
70
+ ValueError: If value cannot be parsed as integer
71
+ """
72
+ return int(value)
73
+
74
+
75
+ def parse_list(value: str) -> list[str]:
76
+ """Parse list from comma-separated string.
77
+
78
+ Args:
79
+ value: Comma-separated string
80
+
81
+ Returns:
82
+ List of strings (trimmed)
83
+ """
84
+ return [item.strip() for item in value.split(",") if item.strip()]
85
+
86
+
87
+ def parse_value(value: str, expected_type: type) -> Any:
88
+ """Parse environment variable value based on expected type.
89
+
90
+ Args:
91
+ value: String value from environment
92
+ expected_type: Expected Python type
93
+
94
+ Returns:
95
+ Parsed value of appropriate type
96
+
97
+ Raises:
98
+ ValueError: If value cannot be parsed to expected type
99
+ """
100
+ if expected_type is bool:
101
+ return parse_bool(value)
102
+ elif expected_type is int:
103
+ return parse_int(value)
104
+ elif expected_type is list:
105
+ return parse_list(value)
106
+ else:
107
+ return value # String, return as-is
108
+
109
+
110
+ def load_env_config() -> dict[str, dict[str, Any]]:
111
+ """Load configuration from environment variables.
112
+
113
+ Reads all OASR_* environment variables and converts them to a nested
114
+ dictionary structure matching the config file format.
115
+
116
+ Returns:
117
+ Nested dictionary with config values from environment variables
118
+ Example: {"agent": {"default": "codex"}, "oasr": {"default_profile": "safe"}}
119
+
120
+ Notes:
121
+ - Only processes variables defined in ENV_VAR_MAP
122
+ - Silently skips variables with invalid values (logs warning)
123
+ - Returns empty sections if no relevant env vars set
124
+ """
125
+ config = {}
126
+
127
+ for env_var, (section, key) in ENV_VAR_MAP.items():
128
+ value = os.getenv(env_var)
129
+ if value is None:
130
+ continue
131
+
132
+ # Determine expected type based on key
133
+ # This is a heuristic - we infer type from key name
134
+ expected_type = str # Default
135
+ if key == "strict":
136
+ expected_type = bool
137
+ elif key == "reference_max_lines":
138
+ expected_type = int
139
+ elif key == "default_targets":
140
+ expected_type = list
141
+
142
+ try:
143
+ parsed_value = parse_value(value, expected_type)
144
+
145
+ # Create section if it doesn't exist
146
+ if section not in config:
147
+ config[section] = {}
148
+
149
+ config[section][key] = parsed_value
150
+
151
+ except (ValueError, TypeError) as e:
152
+ # Log warning but continue (fail-safe)
153
+ import sys
154
+
155
+ print(
156
+ f"⚠ Warning: Invalid value for {env_var}='{value}': {e}. Skipping.",
157
+ file=sys.stderr,
158
+ )
159
+ continue
160
+
161
+ return config
162
+
163
+
164
+ def merge_configs(
165
+ cli_overrides: dict[str, Any],
166
+ env_config: dict[str, dict[str, Any]],
167
+ file_config: dict[str, dict[str, Any]],
168
+ defaults: dict[str, dict[str, Any]],
169
+ ) -> dict[str, dict[str, Any]]:
170
+ """Merge configurations from multiple sources with correct precedence.
171
+
172
+ Precedence order (highest to lowest):
173
+ 1. cli_overrides - explicit CLI flags
174
+ 2. env_config - environment variables
175
+ 3. file_config - config file values
176
+ 4. defaults - built-in defaults
177
+
178
+ Args:
179
+ cli_overrides: Values explicitly set via CLI flags
180
+ env_config: Values from environment variables
181
+ file_config: Values from config file
182
+ defaults: Built-in default values
183
+
184
+ Returns:
185
+ Merged configuration dictionary
186
+
187
+ Notes:
188
+ - CLI overrides take precedence over everything
189
+ - Environment variables override config file
190
+ - Config file overrides defaults
191
+ - Sections are merged independently
192
+ """
193
+ result = {}
194
+
195
+ # Get all sections from all sources
196
+ all_sections = set()
197
+ for config in [defaults, file_config, env_config, cli_overrides]:
198
+ all_sections.update(config.keys())
199
+
200
+ # Merge each section with precedence
201
+ for section in all_sections:
202
+ result[section] = {}
203
+
204
+ # Start with defaults
205
+ if section in defaults:
206
+ result[section].update(defaults[section])
207
+
208
+ # Override with file config
209
+ if section in file_config:
210
+ result[section].update(file_config[section])
211
+
212
+ # Override with env config
213
+ if section in env_config:
214
+ result[section].update(env_config[section])
215
+
216
+ # Override with CLI (if present)
217
+ if section in cli_overrides:
218
+ result[section].update(cli_overrides[section])
219
+
220
+ return result
221
+
222
+
223
+ def get_config_source(
224
+ section: str,
225
+ key: str,
226
+ cli_overrides: dict[str, Any],
227
+ env_config: dict[str, dict[str, Any]],
228
+ file_config: dict[str, dict[str, Any]],
229
+ ) -> str:
230
+ """Determine the source of a config value.
231
+
232
+ Args:
233
+ section: Config section name
234
+ key: Config key name
235
+ cli_overrides: CLI flag overrides
236
+ env_config: Environment variable config
237
+ file_config: Config file values
238
+
239
+ Returns:
240
+ Source string: "cli flag", "env var", "config file", or "default"
241
+ """
242
+ if section in cli_overrides and key in cli_overrides[section]:
243
+ return "cli flag"
244
+ if section in env_config and key in env_config.get(section, {}):
245
+ return "env var"
246
+ if section in file_config and key in file_config.get(section, {}):
247
+ return "config file"
248
+ return "default"
config/schema.py CHANGED
@@ -5,6 +5,44 @@ from typing import Any
5
5
  VALID_AGENTS = {"codex", "copilot", "claude", "opencode"}
6
6
 
7
7
 
8
+ def validate_agent(agent: str | None) -> tuple[bool, str | None]:
9
+ """Validate agent configuration value.
10
+
11
+ Args:
12
+ agent: Agent name to validate
13
+
14
+ Returns:
15
+ Tuple of (is_valid, error_message)
16
+ """
17
+ if agent is None:
18
+ return (True, None)
19
+
20
+ if agent not in VALID_AGENTS:
21
+ sorted_agents = ", ".join(sorted(VALID_AGENTS))
22
+ return (False, f"Invalid agent '{agent}'. Valid agents: {sorted_agents}")
23
+
24
+ return (True, None)
25
+
26
+
27
+ def validate_profile_reference(profile_name: str, config: dict[str, Any]) -> tuple[bool, str | None]:
28
+ """Validate that a profile reference exists in config.
29
+
30
+ Args:
31
+ profile_name: Profile name to validate
32
+ config: Full config dictionary
33
+
34
+ Returns:
35
+ Tuple of (is_valid, error_message)
36
+ """
37
+ profiles = config.get("profiles", {})
38
+
39
+ if profile_name not in profiles:
40
+ available = ", ".join(sorted(profiles.keys()))
41
+ return (False, f"Profile '{profile_name}' not found. Available profiles: {available}")
42
+
43
+ return (True, None)
44
+
45
+
8
46
  def validate_config(config: dict[str, Any]) -> None:
9
47
  """Validate configuration dictionary.
10
48
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oasr
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: CLI for managing agent skills across IDE integrations
5
5
  Project-URL: Homepage, https://github.com/jgodau/asr
6
6
  Project-URL: Repository, https://github.com/jgodau/asr
@@ -317,6 +317,26 @@ Remote skills are fetched on-demand during `adapter` and `use` operations. The r
317
317
 
318
318
  ---
319
319
 
320
+ ## Shell Completions
321
+
322
+ OASR supports intelligent tab completion for Bash, Zsh, Fish, and PowerShell:
323
+
324
+ ```bash
325
+ # Install for your current shell
326
+ oasr completion install
327
+
328
+ # Now try it:
329
+ oasr <TAB> # Complete commands
330
+ oasr info <TAB> # Complete skill names
331
+ oasr exec --<TAB> # Complete flags
332
+ ```
333
+
334
+ Completions are **dynamic** — skill names, agents, and profiles are fetched live from your registry.
335
+
336
+ See [`oasr completion --help`](docs/commands/COMPLETION.md) for details.
337
+
338
+ ---
339
+
320
340
  ## Documentation
321
341
 
322
342
  - **[Quickstart](docs/QUICKSTART.md)** — Installation and first steps
@@ -1,7 +1,7 @@
1
1
  __init__.py,sha256=cYuwXNht5J2GDPEbHz57rmXRyWzaUgAaCXz8okR0rKE,84
2
2
  __main__.py,sha256=Due_Us-4KNlLZhf8MkmoP1hWS5qMWmpZvz2ZaCqPHT4,120
3
3
  adapter.py,sha256=WEpYkKDTb7We0zU9i6Z-r5ydtUdghNhxTZ5Eq58h4fU,10027
4
- cli.py,sha256=r5LsYxPe5-y1sqAg-IrII3J5Cf7Y2z3KyICkyaI0U-U,2672
4
+ cli.py,sha256=LQg93XqUurMvcga2gV6sebv2RXGqfwDNvCN_UOLeTUc,2820
5
5
  discovery.py,sha256=WWF8SN2LH88mOUBJLavM7rvXcxi6uDQGpqRK20GysxA,3298
6
6
  manifest.py,sha256=feNCjkFWfhoVubevKjLtKoIEuzT1YGQn6wWgs9XM8_o,12229
7
7
  registry.py,sha256=zGutwVP39xaYqc3KDEXMWCV1tORYpqc5JISO8OaWP1Q,4470
@@ -27,9 +27,10 @@ commands/adapter.py,sha256=_68v3t-dRU0mszzL4udKs1bKennyg7RfBTaK2fDGTsE,3215
27
27
  commands/add.py,sha256=NJLQ-8-3zy7o6S9VLfL_wauP-Vz0oNGwN3nvtiwxNYM,15255
28
28
  commands/clean.py,sha256=RQBAfe6iCLsjMqUyVR55JdYX9MBqgrUuIrA8rFKs1J0,1102
29
29
  commands/clone.py,sha256=4APH34-yHjiXQIQwBnKOSEQ_sxV24_GKypcOJMfncvs,5912
30
- commands/config.py,sha256=PKuOX7CPRAy2j5NG1rhHYDFJT1XvZnOTF2qJW04v34Q,4940
30
+ commands/completion.py,sha256=Y2KshaJ64vI1fcTR5z2KTJT7u9PPK_w8qMf5HK_q9ns,8570
31
+ commands/config.py,sha256=4kzDEjVpwrmMPK_DPYePdQe2lGh_b8waYORZDHCDYZw,6976
31
32
  commands/diff.py,sha256=37JMjvfAEfvK7-4X5iFbD-IGkS8ae4YSY7ZDIZF5B9E,5766
32
- commands/exec.py,sha256=Su5IVQCUR2oKktFG4gCRA4A15-4-WTaCHpqRLONTnnY,8230
33
+ commands/exec.py,sha256=zFmxxclpHQF39sqDpR5436XQiEYo334BGcQ5a8gbR9I,8711
33
34
  commands/find.py,sha256=zgqwUnaG5aLX6gJIU2ZeQzxsFh2s7oDNNtmV-e-62Jg,1663
34
35
  commands/help.py,sha256=5yhIpgGs1xPs2f39lg-ELE7D0tV_uUTjxQsgkWusIwo,1449
35
36
  commands/info.py,sha256=zywaUQsrvcPXcX8W49P7Jqnr90pX8nBPqnH1XcIs0Uk,4396
@@ -41,9 +42,15 @@ commands/sync.py,sha256=ZQoB5hBqrzvM6LUQVlKqHQVJib4dB5qe5M-pVG2vtGM,4946
41
42
  commands/update.py,sha256=bOWjdTNyeYg-hvXv5GfUzEtsTA7gU9JLM592GI9Oq68,11939
42
43
  commands/use.py,sha256=ggB28g2BDg3Lv3nF40wnDAJ7p0mo6C1pc1KgahvQYXM,1452
43
44
  commands/validate.py,sha256=Y8TLHxW4Z98onmzu-h-kDIET-48lVaIdQXOvuyBemLw,2361
44
- config/__init__.py,sha256=kll8gcMJX9qn3Y4WJCieFP4hUdioe_RXGqEU1zFjOPI,3473
45
+ completions/__init__.py,sha256=cLGMHifEf91ElOIMSVffVWcifjGZ7oStb0Ot4ivLmmE,41
46
+ completions/bash.sh,sha256=gqwuOqUDayPQ5_fHUtttJ_AK-Jef-vgSkUATbbtJBic,6169
47
+ completions/fish.fish,sha256=LXUPp3Ad8aJ7j9m_xlX9Ej9H-3xLUhhMSzsB02oH8Vw,8042
48
+ completions/powershell.ps1,sha256=dRtY-w8Hd3EmmJTtoDdkYzFN6tYiUtCeuu0pufUFsps,4210
49
+ completions/zsh.sh,sha256=Pt-jfSHy8Q5kRtn314ejmP3_VCQGzcHAmK_buOKE4JI,6751
50
+ config/__init__.py,sha256=glSjT1_y4aOfhZ8odrUWCGF1hBbY_huTjVp6suepHDY,3647
45
51
  config/defaults.py,sha256=JfCltQYoE7EqBYlxsNrSITLmwifTvRrJe5lqL0Ys7Cs,986
46
- config/schema.py,sha256=7nXgT4m_k04VZl3XJF_v8W4YMwSFGoCqQMLZAOdLuKk,3445
52
+ config/env.py,sha256=WgnQXjhfvV7m1oxZCK9WdIX_rqLy_-BOSuPjbpjdI1c,7163
53
+ config/schema.py,sha256=VlvmiYWjU2hExBJfME90Oyqp-H4OHcUs_hvvp54K9jA,4498
47
54
  policy/__init__.py,sha256=0sPJaruOyc9ioNyIcrTW72RgpaE64FgibS0h5mQELb8,1353
48
55
  policy/defaults.py,sha256=9GMQM2l2OKTmhXlKwyTfcICR5vD9qEvyvqaR5KrN7ZI,620
49
56
  policy/enforcement.py,sha256=djsosjjfdyr0SjnHF2kz4u3glvMNgd1CJztN6yZE-fM,2749
@@ -51,9 +58,9 @@ policy/profile.py,sha256=WDKaUagsWnBPGz5a_OOcxTsdZ66WjaIaR0R7ITVqy8g,6790
51
58
  skillcopy/__init__.py,sha256=YUglUkDzKfnCt4ar_DU33ksI9fGyn2UYbV7qn2c_BcU,2322
52
59
  skillcopy/local.py,sha256=QH6484dCenjg8pfNOyTRbQQBklEWhkkTnfQok5ssf_4,1049
53
60
  skillcopy/remote.py,sha256=83jRA2VfjtSDGO-YM1x3WGJjKvWzK1RmSTL7SdUOz8s,3155
54
- oasr-0.5.0.dist-info/METADATA,sha256=rSs47OQ6xzJsAGud17O43UTi0WF6ZLCKlE6_MK-ROaE,17924
55
- oasr-0.5.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
56
- oasr-0.5.0.dist-info/entry_points.txt,sha256=VnMuOi6XYMbzAD2bP0X5uV1sQXjOqoDWJ33Lsxwq8u8,52
57
- oasr-0.5.0.dist-info/licenses/LICENSE,sha256=nQ1j9Ldb8FlJ-z7y2WuXPIlyfnYC7YPasjGdOBgcfP4,10561
58
- oasr-0.5.0.dist-info/licenses/NOTICE,sha256=EsfkCN0ZRDS0oj3ADvMKeKrAXaPlC8YfpSjvjGVv9jE,207
59
- oasr-0.5.0.dist-info/RECORD,,
61
+ oasr-0.5.2.dist-info/METADATA,sha256=SQ26Gtaa3LH-7f5CSHfM1yLUzNmizhYEuWO9lqTqSVM,18413
62
+ oasr-0.5.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
63
+ oasr-0.5.2.dist-info/entry_points.txt,sha256=VnMuOi6XYMbzAD2bP0X5uV1sQXjOqoDWJ33Lsxwq8u8,52
64
+ oasr-0.5.2.dist-info/licenses/LICENSE,sha256=nQ1j9Ldb8FlJ-z7y2WuXPIlyfnYC7YPasjGdOBgcfP4,10561
65
+ oasr-0.5.2.dist-info/licenses/NOTICE,sha256=EsfkCN0ZRDS0oj3ADvMKeKrAXaPlC8YfpSjvjGVv9jE,207
66
+ oasr-0.5.2.dist-info/RECORD,,
File without changes