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.
cli.py CHANGED
@@ -13,7 +13,7 @@ from pathlib import Path
13
13
  from commands import adapter, clean, clone, config, diff, exec, find, registry, sync, update, use, validate
14
14
  from commands import help as help_cmd
15
15
 
16
- __version__ = "0.5.0"
16
+ __version__ = "0.5.2"
17
17
 
18
18
 
19
19
  def main(argv: list[str] | None = None) -> int:
@@ -88,6 +88,11 @@ def create_parser() -> argparse.ArgumentParser:
88
88
 
89
89
  info_cmd.register(subparsers)
90
90
 
91
+ # Import and register completion command
92
+ from commands import completion as completion_cmd
93
+
94
+ completion_cmd.register_parser(subparsers)
95
+
91
96
  help_cmd.register(subparsers, parser)
92
97
 
93
98
  return parser
commands/completion.py ADDED
@@ -0,0 +1,345 @@
1
+ """Shell completion management for OASR."""
2
+
3
+ import os
4
+ import platform
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from config import load_config
9
+
10
+
11
+ def register_parser(subparsers):
12
+ """Register the completion subcommand."""
13
+ parser = subparsers.add_parser(
14
+ "completion",
15
+ help="Generate and install shell completions",
16
+ description="Generate shell completion scripts for bash, zsh, fish, and PowerShell.",
17
+ )
18
+
19
+ parser.add_argument(
20
+ "shell",
21
+ nargs="?",
22
+ choices=["bash", "zsh", "fish", "powershell", "install", "uninstall"],
23
+ help="Shell type or action (install/uninstall)",
24
+ )
25
+
26
+ parser.add_argument(
27
+ "--force",
28
+ action="store_true",
29
+ help="Force installation even if already installed",
30
+ )
31
+
32
+ parser.add_argument(
33
+ "--dry-run",
34
+ action="store_true",
35
+ help="Show what would be done without doing it",
36
+ )
37
+
38
+ parser.set_defaults(func=run)
39
+
40
+
41
+ def detect_shell():
42
+ """
43
+ Auto-detect the user's shell based on platform and environment.
44
+
45
+ Returns:
46
+ str: Shell name (bash, zsh, fish, powershell)
47
+ """
48
+ if platform.system() == "Windows":
49
+ shell_env = os.environ.get("SHELL", "").lower()
50
+ if "bash" in shell_env:
51
+ return "bash"
52
+ # Check for PowerShell
53
+ if "PSModulePath" in os.environ or "POWERSHELL" in os.environ.get("SHELL", "").upper():
54
+ return "powershell"
55
+ if "zsh" in shell_env:
56
+ return "zsh"
57
+ if "fish" in shell_env:
58
+ return "fish"
59
+ # Default to PowerShell on Windows
60
+ return "powershell"
61
+ else:
62
+ # Linux/macOS
63
+ shell = os.environ.get("SHELL", "")
64
+ if "zsh" in shell:
65
+ return "zsh"
66
+ elif "fish" in shell:
67
+ return "fish"
68
+ else:
69
+ # Default to bash on Unix
70
+ return "bash"
71
+
72
+
73
+ def get_completion_script(shell):
74
+ """
75
+ Get the completion script content for the specified shell.
76
+
77
+ Args:
78
+ shell: Shell name (bash, zsh, fish, powershell)
79
+
80
+ Returns:
81
+ str: Completion script content
82
+ """
83
+ # Map shell names to completion files
84
+ script_files = {
85
+ "bash": "bash.sh",
86
+ "zsh": "zsh.sh",
87
+ "fish": "fish.fish",
88
+ "powershell": "powershell.ps1",
89
+ }
90
+
91
+ if shell not in script_files:
92
+ return ""
93
+
94
+ # Get the path to the completion script
95
+ completion_file = Path(__file__).parent.parent / "completions" / script_files[shell]
96
+
97
+ if not completion_file.exists():
98
+ return f"# {shell} completion script not found"
99
+
100
+ return completion_file.read_text()
101
+
102
+
103
+ def get_completion_path(shell, system=False):
104
+ """
105
+ Get the installation path for completion script.
106
+
107
+ Args:
108
+ shell: Shell name
109
+ system: If True, use system-wide path (requires root)
110
+
111
+ Returns:
112
+ Path: Installation path
113
+ """
114
+ paths = {
115
+ "bash": {
116
+ "user": Path.home() / ".bash_completion.d" / "oasr",
117
+ "system": Path("/etc/bash_completion.d/oasr"),
118
+ },
119
+ "zsh": {
120
+ "user": Path.home() / ".zsh" / "completion" / "_oasr",
121
+ "system": Path("/usr/local/share/zsh/site-functions/_oasr"),
122
+ },
123
+ "fish": {
124
+ "user": Path.home() / ".config" / "fish" / "completions" / "oasr.fish",
125
+ "system": Path("/usr/share/fish/vendor_completions.d/oasr.fish"),
126
+ },
127
+ "powershell": {
128
+ "user": Path.home() / ".config" / "powershell" / "oasr_completion.ps1",
129
+ "system": Path.home() / ".config" / "powershell" / "oasr_completion.ps1",
130
+ },
131
+ }
132
+
133
+ level = "system" if system else "user"
134
+ return paths.get(shell, {}).get(level)
135
+
136
+
137
+ def print_activation_instructions(shell, path):
138
+ """
139
+ Print instructions for activating completions.
140
+
141
+ Args:
142
+ shell: Shell name
143
+ path: Installation path
144
+ """
145
+ instructions = {
146
+ "bash": f"""
147
+ ✓ Completions installed for bash
148
+
149
+ To activate, add this to your ~/.bashrc:
150
+ source {path}
151
+
152
+ Or run:
153
+ echo 'source {path}' >> ~/.bashrc
154
+
155
+ Then restart your shell or run:
156
+ source ~/.bashrc
157
+ """,
158
+ "zsh": f"""
159
+ ✓ Completions installed for zsh
160
+
161
+ To activate, add this to your ~/.zshrc:
162
+ fpath=({path.parent} $fpath)
163
+ autoload -Uz compinit && compinit
164
+
165
+ Then restart your shell or run:
166
+ source ~/.zshrc
167
+ """,
168
+ "fish": f"""
169
+ ✓ Completions installed for fish
170
+
171
+ Completions will be active in new fish shells.
172
+ To activate now, run:
173
+ source {path}
174
+ """,
175
+ "powershell": f"""
176
+ ✓ Completions installed for PowerShell
177
+
178
+ To activate, add this to your PowerShell profile ($PROFILE):
179
+ . {path}
180
+
181
+ Or run:
182
+ echo '. {path}' >> $PROFILE
183
+
184
+ Then restart PowerShell or run:
185
+ . $PROFILE
186
+ """,
187
+ }
188
+
189
+ print(instructions.get(shell, ""))
190
+
191
+
192
+ def is_already_installed(shell):
193
+ """
194
+ Check if completions are already installed for the shell.
195
+
196
+ Args:
197
+ shell: Shell name
198
+
199
+ Returns:
200
+ bool: True if already installed
201
+ """
202
+ path = get_completion_path(shell)
203
+ if not path or not path.exists():
204
+ return False
205
+
206
+ # Check for our signature
207
+ with open(path) as f:
208
+ content = f.read()
209
+ return "# oasr completion" in content.lower()
210
+
211
+
212
+ def run_output(shell):
213
+ """
214
+ Output the completion script to stdout.
215
+
216
+ Args:
217
+ shell: Shell name
218
+
219
+ Returns:
220
+ int: Exit code
221
+ """
222
+ script = get_completion_script(shell)
223
+ print(script)
224
+ return 0
225
+
226
+
227
+ def run_install(args):
228
+ """
229
+ Install completion script.
230
+
231
+ Args:
232
+ args: Parsed arguments
233
+
234
+ Returns:
235
+ int: Exit code
236
+ """
237
+ shell = args.shell if args.shell != "install" else detect_shell()
238
+
239
+ # Check config
240
+ config = load_config()
241
+ if not config.get("oasr", {}).get("completions", True):
242
+ print("Completions are disabled in config (oasr.completions = false)")
243
+ return 1
244
+
245
+ # Check if already installed
246
+ if is_already_installed(shell) and not args.force:
247
+ print(f"✓ Completions already installed for {shell}")
248
+ print(" Use --force to reinstall")
249
+ return 0
250
+
251
+ # Get paths
252
+ path = get_completion_path(shell)
253
+ if not path:
254
+ print(f"Error: Unsupported shell: {shell}", file=sys.stderr)
255
+ return 1
256
+
257
+ # Dry run
258
+ if args.dry_run:
259
+ print(f"Would install completion to: {path}")
260
+ print_activation_instructions(shell, path)
261
+ return 0
262
+
263
+ # Get script
264
+ script = get_completion_script(shell)
265
+
266
+ # Create directory
267
+ path.parent.mkdir(parents=True, exist_ok=True)
268
+
269
+ # Backup existing
270
+ if path.exists() and not is_already_installed(shell):
271
+ import time
272
+
273
+ backup = path.parent / f"{path.name}.backup.{int(time.time())}"
274
+ path.rename(backup)
275
+ print(f"Backed up existing file to: {backup}")
276
+
277
+ # Write script
278
+ path.write_text(script)
279
+
280
+ # Success message
281
+ print_activation_instructions(shell, path)
282
+
283
+ return 0
284
+
285
+
286
+ def run_uninstall(args):
287
+ """
288
+ Uninstall completion script.
289
+
290
+ Args:
291
+ args: Parsed arguments
292
+
293
+ Returns:
294
+ int: Exit code
295
+ """
296
+ shell = args.shell if args.shell != "uninstall" else detect_shell()
297
+
298
+ path = get_completion_path(shell)
299
+ if not path or not path.exists():
300
+ print(f"No completions installed for {shell}")
301
+ return 0
302
+
303
+ # Dry run
304
+ if args.dry_run:
305
+ print(f"Would remove: {path}")
306
+ return 0
307
+
308
+ # Remove file
309
+ path.unlink()
310
+ print(f"✓ Removed completions for {shell}")
311
+
312
+ return 0
313
+
314
+
315
+ def run(args):
316
+ """
317
+ Execute completion command.
318
+
319
+ Args:
320
+ args: Parsed arguments
321
+
322
+ Returns:
323
+ int: Exit code
324
+ """
325
+ # No shell specified - show help
326
+ if not args.shell:
327
+ detected = detect_shell()
328
+ print(f"Detected shell: {detected}")
329
+ print("\nUsage:")
330
+ print(" oasr completion bash # Output bash completion script")
331
+ print(" oasr completion zsh # Output zsh completion script")
332
+ print(" oasr completion fish # Output fish completion script")
333
+ print(" oasr completion powershell # Output PowerShell completion script")
334
+ print(" oasr completion install # Auto-detect and install")
335
+ print(" oasr completion uninstall # Remove installed completions")
336
+ return 0
337
+
338
+ # Handle actions
339
+ if args.shell == "install":
340
+ return run_install(args)
341
+ elif args.shell == "uninstall":
342
+ return run_uninstall(args)
343
+ else:
344
+ # Output script to stdout
345
+ return run_output(args.shell)
commands/config.py CHANGED
@@ -3,8 +3,9 @@
3
3
  import argparse
4
4
  import sys
5
5
 
6
- from agents import detect_available_agents, get_all_agent_names
6
+ from agents import detect_available_agents
7
7
  from config import CONFIG_FILE, load_config, save_config
8
+ from config.schema import validate_agent, validate_profile_reference
8
9
 
9
10
 
10
11
  def register(subparsers: argparse._SubParsersAction) -> None:
@@ -25,6 +26,11 @@ def register(subparsers: argparse._SubParsersAction) -> None:
25
26
  )
26
27
  set_parser.add_argument("key", help="Configuration key (e.g., 'agent')")
27
28
  set_parser.add_argument("value", help="Configuration value")
29
+ set_parser.add_argument(
30
+ "--force",
31
+ action="store_true",
32
+ help="Skip validation (use carefully)",
33
+ )
28
34
  set_parser.set_defaults(func=run_set)
29
35
 
30
36
  # config get
@@ -57,40 +63,86 @@ def register(subparsers: argparse._SubParsersAction) -> None:
57
63
 
58
64
 
59
65
  def run_set(args: argparse.Namespace) -> int:
60
- """Set a configuration value."""
66
+ """Set a configuration value with validation."""
61
67
  key = args.key.lower()
62
68
  value = args.value
69
+ force = getattr(args, "force", False)
63
70
 
64
- # Only support agent for now
65
- if key == "agent":
66
- # Validate agent name
67
- valid_agents = get_all_agent_names()
68
- if value not in valid_agents:
69
- print(
70
- f"Error: Invalid agent '{value}'. Must be one of: {', '.join(valid_agents)}",
71
- file=sys.stderr,
72
- )
71
+ # Parse key (support dotted notation like "validation.strict")
72
+ if "." in key:
73
+ parts = key.split(".", 1)
74
+ if len(parts) != 2:
75
+ print(f"Error: Invalid key '{key}'. Use format 'section.field' or 'agent'", file=sys.stderr)
73
76
  return 1
77
+ section, field = parts
78
+ elif key == "agent":
79
+ # Special case: bare "agent" means "agent.default"
80
+ section, field = "agent", "default"
81
+ else:
82
+ print(f"Error: Invalid key '{key}'. Use format 'section.field' or 'agent'", file=sys.stderr)
83
+ return 1
74
84
 
75
- # Load config, update, save
76
- config = load_config(args.config if hasattr(args, "config") else None)
77
- config["agent"]["default"] = value
78
- save_config(config, args.config if hasattr(args, "config") else None)
85
+ # Type coercion based on field
86
+ original_value = value
87
+ if field == "strict":
88
+ value = value.lower() in ("true", "1", "yes", "on")
89
+ elif field == "reference_max_lines":
90
+ try:
91
+ value = int(value)
92
+ if value < 1:
93
+ print(f"Error: '{field}' must be a positive integer", file=sys.stderr)
94
+ return 1
95
+ except ValueError:
96
+ print(f"Error: '{field}' must be an integer", file=sys.stderr)
97
+ return 1
79
98
 
80
- # Show available vs configured
81
- available = detect_available_agents()
82
- if value in available:
83
- print(f"✓ Default agent set to: {value}")
99
+ # Load config
100
+ config_path = getattr(args, "config", None)
101
+ config = load_config(config_path=config_path)
102
+
103
+ # Validate before setting (unless --force)
104
+ if not force:
105
+ # Validate agent
106
+ if section == "agent" and field == "default":
107
+ is_valid, error_msg = validate_agent(value)
108
+ if not is_valid:
109
+ print(f"Error: {error_msg}", file=sys.stderr)
110
+ print("\nTo set anyway, use: oasr config set --force agent <name>", file=sys.stderr)
111
+ return 1
112
+
113
+ # Validate profile reference
114
+ if section == "oasr" and field == "default_profile":
115
+ is_valid, error_msg = validate_profile_reference(value, config)
116
+ if not is_valid:
117
+ print(f"Error: {error_msg}", file=sys.stderr)
118
+ print("\nCreate the profile in ~/.oasr/config.toml first, or use:", file=sys.stderr)
119
+ print(f" oasr config set --force oasr.default_profile {value}", file=sys.stderr)
120
+ return 1
121
+
122
+ # Set the value
123
+ if section not in config:
124
+ config[section] = {}
125
+
126
+ config[section][field] = value
127
+
128
+ try:
129
+ save_config(config, config_path=config_path)
130
+
131
+ # Show confirmation
132
+ if section == "agent" and field == "default":
133
+ # Special handling for agent - check if available
134
+ available = detect_available_agents()
135
+ if value in available:
136
+ print(f"✓ Default agent set to: {value}")
137
+ else:
138
+ print(f"✓ Default agent set to: {value}")
139
+ print(f" Warning: '{value}' binary not found in PATH. Install it to use this agent.", file=sys.stderr)
84
140
  else:
85
- print(f"✓ Default agent set to: {value}")
86
- print(
87
- f" Warning: '{value}' binary not found in PATH. Install it to use this agent.",
88
- file=sys.stderr,
89
- )
141
+ print(f"✓ Set {section}.{field} = {original_value}")
90
142
 
91
143
  return 0
92
- else:
93
- print(f"Error: Unsupported config key '{key}'. Only 'agent' is supported.", file=sys.stderr)
144
+ except ValueError as e:
145
+ print(f"Error: {e}", file=sys.stderr)
94
146
  return 1
95
147
 
96
148
 
commands/exec.py CHANGED
@@ -101,18 +101,30 @@ def run(args: argparse.Namespace) -> int:
101
101
  # Error already printed by _get_user_prompt
102
102
  return 1
103
103
 
104
- # Determine which agent to use
105
- agent_name = _get_agent_name(args)
106
- if agent_name is None:
107
- # Error already printed by _get_agent_name
108
- return 1
109
-
110
104
  # === POLICY ENFORCEMENT ===
111
- # Load configuration
112
- config = load_config()
105
+ # Build CLI overrides for config loading
106
+ cli_overrides = {}
107
+ if args.agent:
108
+ cli_overrides["agent"] = {"default": args.agent}
109
+ if args.profile:
110
+ cli_overrides["oasr"] = cli_overrides.get("oasr", {})
111
+ cli_overrides["oasr"]["default_profile"] = args.profile
112
+
113
+ # Load configuration with precedence: CLI > env > file > defaults
114
+ config = load_config(cli_overrides=cli_overrides)
113
115
 
114
- # Determine which profile to use (flag overrides config)
115
- profile_name = args.profile if args.profile else config.get("oasr", {}).get("default_profile", "safe")
116
+ # Determine agent and profile from merged config
117
+ agent_name = config.get("agent", {}).get("default")
118
+ profile_name = config.get("oasr", {}).get("default_profile", "safe")
119
+
120
+ # Validate agent is set
121
+ if not agent_name:
122
+ print(
123
+ "Error: No agent configured. Set OASR_AGENT, use --agent flag, or run:",
124
+ file=sys.stderr,
125
+ )
126
+ print(" oasr config set agent <name>", file=sys.stderr)
127
+ return 1
116
128
 
117
129
  # Load the policy profile
118
130
  profile = policy.load(config, profile_name)
@@ -0,0 +1 @@
1
+ """Shell completion scripts for OASR."""