oasr 0.5.2__py3-none-any.whl → 0.6.0__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.
agents/base.py CHANGED
@@ -34,25 +34,39 @@ class AgentDriver(ABC):
34
34
  return shutil.which(self.get_binary_name()) is not None
35
35
 
36
36
  @abstractmethod
37
- def build_command(self, skill_content: str, user_prompt: str, cwd: Path) -> list[str]:
37
+ def build_command(
38
+ self,
39
+ skill_content: str,
40
+ user_prompt: str,
41
+ cwd: Path,
42
+ extra_args: list[str] | None = None,
43
+ ) -> list[str]:
38
44
  """Build the command to execute.
39
45
 
40
46
  Args:
41
47
  skill_content: Full SKILL.md content.
42
48
  user_prompt: User's prompt/request.
43
49
  cwd: Current working directory.
50
+ extra_args: Additional CLI arguments for the agent (optional).
44
51
 
45
52
  Returns:
46
53
  Command as list of strings (for subprocess).
47
54
  """
48
55
 
49
- def execute(self, skill_content: str, user_prompt: str, cwd: Path | None = None) -> subprocess.CompletedProcess:
56
+ def execute(
57
+ self,
58
+ skill_content: str,
59
+ user_prompt: str,
60
+ cwd: Path | None = None,
61
+ extra_args: list[str] | None = None,
62
+ ) -> subprocess.CompletedProcess:
50
63
  """Execute skill with agent.
51
64
 
52
65
  Args:
53
66
  skill_content: Full SKILL.md content.
54
67
  user_prompt: User's prompt/request.
55
68
  cwd: Working directory for execution (defaults to current dir).
69
+ extra_args: Additional CLI arguments for the agent (optional).
56
70
 
57
71
  Returns:
58
72
  CompletedProcess with stdout/stderr/returncode.
@@ -64,7 +78,7 @@ class AgentDriver(ABC):
64
78
  raise FileNotFoundError(f"{self.get_name()} binary '{self.get_binary_name()}' not found in PATH")
65
79
 
66
80
  working_dir = cwd or Path.cwd()
67
- cmd = self.build_command(skill_content, user_prompt, working_dir)
81
+ cmd = self.build_command(skill_content, user_prompt, working_dir, extra_args=extra_args)
68
82
 
69
83
  return subprocess.run(
70
84
  cmd,
agents/claude.py CHANGED
@@ -16,10 +16,20 @@ class ClaudeDriver(AgentDriver):
16
16
  """Get the CLI binary name."""
17
17
  return "claude"
18
18
 
19
- def build_command(self, skill_content: str, user_prompt: str, cwd: Path) -> list[str]:
19
+ def build_command(
20
+ self,
21
+ skill_content: str,
22
+ user_prompt: str,
23
+ cwd: Path,
24
+ extra_args: list[str] | None = None,
25
+ ) -> list[str]:
20
26
  """Build claude command.
21
27
 
22
28
  Claude syntax: claude <prompt> -p
23
29
  """
24
30
  injected_prompt = self.format_injected_prompt(skill_content, user_prompt, cwd)
25
- return ["claude", injected_prompt, "-p"]
31
+ cmd = ["claude"]
32
+ if extra_args:
33
+ cmd.extend(extra_args)
34
+ cmd.extend([injected_prompt, "-p"])
35
+ return cmd
agents/codex.py CHANGED
@@ -16,10 +16,20 @@ class CodexDriver(AgentDriver):
16
16
  """Get the CLI binary name."""
17
17
  return "codex"
18
18
 
19
- def build_command(self, skill_content: str, user_prompt: str, cwd: Path) -> list[str]:
19
+ def build_command(
20
+ self,
21
+ skill_content: str,
22
+ user_prompt: str,
23
+ cwd: Path,
24
+ extra_args: list[str] | None = None,
25
+ ) -> list[str]:
20
26
  """Build codex exec command.
21
27
 
22
28
  Codex syntax: codex exec "<prompt>"
23
29
  """
24
30
  injected_prompt = self.format_injected_prompt(skill_content, user_prompt, cwd)
25
- return ["codex", "exec", injected_prompt]
31
+ cmd = ["codex", "exec"]
32
+ if extra_args:
33
+ cmd.extend(extra_args)
34
+ cmd.append(injected_prompt)
35
+ return cmd
agents/copilot.py CHANGED
@@ -16,10 +16,20 @@ class CopilotDriver(AgentDriver):
16
16
  """Get the CLI binary name."""
17
17
  return "copilot"
18
18
 
19
- def build_command(self, skill_content: str, user_prompt: str, cwd: Path) -> list[str]:
19
+ def build_command(
20
+ self,
21
+ skill_content: str,
22
+ user_prompt: str,
23
+ cwd: Path,
24
+ extra_args: list[str] | None = None,
25
+ ) -> list[str]:
20
26
  """Build copilot command.
21
27
 
22
28
  Copilot syntax: copilot -p "<prompt>"
23
29
  """
24
30
  injected_prompt = self.format_injected_prompt(skill_content, user_prompt, cwd)
25
- return ["copilot", "-p", injected_prompt]
31
+ cmd = ["copilot"]
32
+ if extra_args:
33
+ cmd.extend(extra_args)
34
+ cmd.extend(["-p", injected_prompt])
35
+ return cmd
agents/opencode.py CHANGED
@@ -16,10 +16,20 @@ class OpenCodeDriver(AgentDriver):
16
16
  """Get the CLI binary name."""
17
17
  return "opencode"
18
18
 
19
- def build_command(self, skill_content: str, user_prompt: str, cwd: Path) -> list[str]:
19
+ def build_command(
20
+ self,
21
+ skill_content: str,
22
+ user_prompt: str,
23
+ cwd: Path,
24
+ extra_args: list[str] | None = None,
25
+ ) -> list[str]:
20
26
  """Build opencode run command.
21
27
 
22
28
  OpenCode syntax: opencode run "<prompt>"
23
29
  """
24
30
  injected_prompt = self.format_injected_prompt(skill_content, user_prompt, cwd)
25
- return ["opencode", "run", injected_prompt]
31
+ cmd = ["opencode", "run"]
32
+ if extra_args:
33
+ cmd.extend(extra_args)
34
+ cmd.append(injected_prompt)
35
+ return cmd
cli.py CHANGED
@@ -10,10 +10,10 @@ import json
10
10
  import sys
11
11
  from pathlib import Path
12
12
 
13
- from commands import adapter, clean, clone, config, diff, exec, find, registry, sync, update, use, validate
13
+ from commands import adapter, clean, clone, config, diff, exec, find, profile, registry, sync, update, use, validate
14
14
  from commands import help as help_cmd
15
15
 
16
- __version__ = "0.5.2"
16
+ __version__ = "0.6.0"
17
17
 
18
18
 
19
19
  def main(argv: list[str] | None = None) -> int:
@@ -72,6 +72,7 @@ def create_parser() -> argparse.ArgumentParser:
72
72
  diff.register(subparsers) # Show tracked skill status
73
73
  sync.register(subparsers) # Refresh tracked skills
74
74
  config.register(subparsers) # Configuration management
75
+ profile.register(subparsers) # Profile selection
75
76
  clone.register(subparsers) # Clone skills to directory
76
77
  exec.register(subparsers) # Execute skills with agent CLI
77
78
 
commands/config.py CHANGED
@@ -2,10 +2,20 @@
2
2
 
3
3
  import argparse
4
4
  import sys
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ if sys.version_info >= (3, 11):
9
+ import tomllib
10
+ else:
11
+ import tomli as tomllib
5
12
 
6
13
  from agents import detect_available_agents
7
- from config import CONFIG_FILE, load_config, save_config
14
+ from config import CONFIG_FILE, get_default_config, load_config, save_config
8
15
  from config.schema import validate_agent, validate_profile_reference
16
+ from profiles.builtins import BUILTIN_PROFILES
17
+ from profiles.loader import list_profile_files, load_profiles
18
+ from profiles.summary import format_profile_summary, sorted_profile_names
9
19
 
10
20
 
11
21
  def register(subparsers: argparse._SubParsersAction) -> None:
@@ -50,6 +60,63 @@ def register(subparsers: argparse._SubParsersAction) -> None:
50
60
  )
51
61
  list_parser.set_defaults(func=run_list)
52
62
 
63
+ # config agent
64
+ agent_parser = config_subparsers.add_parser(
65
+ "agent",
66
+ help="Show agent configuration",
67
+ description="Show default agent configuration and availability",
68
+ )
69
+ agent_parser.set_defaults(func=run_agent)
70
+
71
+ # config validation
72
+ validation_parser = config_subparsers.add_parser(
73
+ "validation",
74
+ help="Show validation settings",
75
+ description="Show validation configuration settings",
76
+ )
77
+ validation_parser.set_defaults(func=run_validation)
78
+
79
+ # config adapter
80
+ adapter_parser = config_subparsers.add_parser(
81
+ "adapter",
82
+ help="Show adapter settings",
83
+ description="Show adapter configuration settings",
84
+ )
85
+ adapter_parser.set_defaults(func=run_adapter)
86
+
87
+ # config oasr
88
+ oasr_parser = config_subparsers.add_parser(
89
+ "oasr",
90
+ help="Show core OASR settings",
91
+ description="Show core OASR configuration settings",
92
+ )
93
+ oasr_parser.set_defaults(func=run_oasr)
94
+
95
+ # config profiles
96
+ profiles_parser = config_subparsers.add_parser(
97
+ "profiles",
98
+ help="Show execution profiles",
99
+ description="Show available execution policy profiles",
100
+ )
101
+ profiles_parser.add_argument("--names", action="store_true", help="Output profile names only")
102
+ profiles_parser.set_defaults(func=run_profiles)
103
+
104
+ # config man
105
+ man_parser = config_subparsers.add_parser(
106
+ "man",
107
+ help="Show configuration reference",
108
+ description="Show configuration reference with examples",
109
+ )
110
+ man_parser.set_defaults(func=run_man)
111
+
112
+ # config validate
113
+ validate_parser = config_subparsers.add_parser(
114
+ "validate",
115
+ help="Validate config file",
116
+ description="Validate configuration file structure and values",
117
+ )
118
+ validate_parser.set_defaults(func=run_validate)
119
+
53
120
  # config path
54
121
  path_parser = config_subparsers.add_parser(
55
122
  "path",
@@ -62,43 +129,82 @@ def register(subparsers: argparse._SubParsersAction) -> None:
62
129
  parser.set_defaults(func=lambda args: parser.print_help() or 1)
63
130
 
64
131
 
132
+ def _load_file_config(config_path: Path) -> dict[str, Any]:
133
+ if config_path.exists():
134
+ with open(config_path, "rb") as f:
135
+ data = tomllib.load(f)
136
+ if isinstance(data, dict):
137
+ return data
138
+ return {}
139
+
140
+
141
+ def _parse_key(key: str) -> tuple[str, str]:
142
+ key = key.lower()
143
+ aliases = {
144
+ "agent": ("agent", "default"),
145
+ "profile": ("oasr", "default_profile"),
146
+ }
147
+ if key in aliases:
148
+ return aliases[key]
149
+ if "." in key:
150
+ parts = key.split(".", 1)
151
+ if len(parts) != 2:
152
+ raise ValueError(f"Invalid key '{key}'. Use format 'section.field' or 'agent'")
153
+ return parts[0], parts[1]
154
+ raise ValueError(f"Invalid key '{key}'. Use format 'section.field' or 'agent'")
155
+
156
+
157
+ def _format_value(value: Any) -> str:
158
+ if isinstance(value, bool):
159
+ return "true" if value else "false"
160
+ if isinstance(value, list):
161
+ return ", ".join(str(item) for item in value)
162
+ return str(value)
163
+
164
+
165
+ def _parse_bool(value: str, field: str) -> bool:
166
+ value_lower = value.lower()
167
+ if value_lower in ("true", "1", "yes", "on"):
168
+ return True
169
+ if value_lower in ("false", "0", "no", "off"):
170
+ return False
171
+ raise ValueError(f"'{field}' must be a boolean")
172
+
173
+
65
174
  def run_set(args: argparse.Namespace) -> int:
66
175
  """Set a configuration value with validation."""
67
176
  key = args.key.lower()
68
177
  value = args.value
69
178
  force = getattr(args, "force", False)
70
179
 
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)
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)
180
+ try:
181
+ section, field = _parse_key(key)
182
+ except ValueError as exc:
183
+ print(f"Error: {exc}", file=sys.stderr)
83
184
  return 1
84
185
 
85
186
  # Type coercion based on field
86
187
  original_value = value
87
- if field == "strict":
88
- value = value.lower() in ("true", "1", "yes", "on")
89
- elif field == "reference_max_lines":
90
- try:
188
+ try:
189
+ if field in ("strict", "completions"):
190
+ value = _parse_bool(value, field)
191
+ elif field == "reference_max_lines":
91
192
  value = int(value)
92
193
  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
194
+ raise ValueError(f"'{field}' must be a positive integer")
195
+ elif field == "default_targets":
196
+ value = [item.strip() for item in value.split(",") if item.strip()]
197
+ elif field == "default_profile":
198
+ value = value.strip()
199
+ except ValueError as exc:
200
+ print(f"Error: {exc}", file=sys.stderr)
201
+ return 1
98
202
 
99
203
  # Load config
100
204
  config_path = getattr(args, "config", None)
205
+ config_file_path = config_path or CONFIG_FILE
101
206
  config = load_config(config_path=config_path)
207
+ file_config = _load_file_config(config_file_path)
102
208
 
103
209
  # Validate before setting (unless --force)
104
210
  if not force:
@@ -115,18 +221,18 @@ def run_set(args: argparse.Namespace) -> int:
115
221
  is_valid, error_msg = validate_profile_reference(value, config)
116
222
  if not is_valid:
117
223
  print(f"Error: {error_msg}", file=sys.stderr)
118
- print("\nCreate the profile in ~/.oasr/config.toml first, or use:", file=sys.stderr)
224
+ print("\nCreate the profile in ~/.oasr/config.toml or ~/.oasr/profile/, or use:", file=sys.stderr)
119
225
  print(f" oasr config set --force oasr.default_profile {value}", file=sys.stderr)
120
226
  return 1
121
227
 
122
228
  # Set the value
123
- if section not in config:
124
- config[section] = {}
229
+ if section not in file_config:
230
+ file_config[section] = {}
125
231
 
126
- config[section][field] = value
232
+ file_config[section][field] = value
127
233
 
128
234
  try:
129
- save_config(config, config_path=config_path)
235
+ save_config(file_config, config_path=config_file_path)
130
236
 
131
237
  # Show confirmation
132
238
  if section == "agent" and field == "default":
@@ -137,6 +243,8 @@ def run_set(args: argparse.Namespace) -> int:
137
243
  else:
138
244
  print(f"✓ Default agent set to: {value}")
139
245
  print(f" Warning: '{value}' binary not found in PATH. Install it to use this agent.", file=sys.stderr)
246
+ elif section == "oasr" and field == "default_profile":
247
+ print(f"✓ Default profile set to: {value}")
140
248
  else:
141
249
  print(f"✓ Set {section}.{field} = {original_value}")
142
250
 
@@ -152,18 +260,20 @@ def run_get(args: argparse.Namespace) -> int:
152
260
 
153
261
  config = load_config(args.config if hasattr(args, "config") else None)
154
262
 
155
- if key == "agent":
156
- agent = config["agent"].get("default")
157
- if agent:
158
- print(agent)
159
- else:
160
- print("No default agent configured", file=sys.stderr)
161
- return 1
162
- return 0
163
- else:
164
- print(f"Error: Unsupported config key '{key}'. Only 'agent' is supported.", file=sys.stderr)
263
+ try:
264
+ section, field = _parse_key(key)
265
+ except ValueError as exc:
266
+ print(f"Error: {exc}", file=sys.stderr)
165
267
  return 1
166
268
 
269
+ value = config.get(section, {}).get(field)
270
+ if value is None:
271
+ print(f"Config value not set: {section}.{field}", file=sys.stderr)
272
+ return 1
273
+
274
+ print(_format_value(value))
275
+ return 0
276
+
167
277
 
168
278
  def run_list(args: argparse.Namespace) -> int:
169
279
  """List all configuration."""
@@ -202,9 +312,146 @@ def run_list(args: argparse.Namespace) -> int:
202
312
  print(f" default_targets = {config['adapter']['default_targets']}")
203
313
  print()
204
314
 
315
+ # OASR section
316
+ print(" [oasr]")
317
+ print(f" default_profile = {config['oasr']['default_profile']}")
318
+ print(f" completions = {_format_value(config['oasr']['completions'])}")
319
+ print()
320
+
321
+ # Profiles section
322
+ print(" [profiles]")
323
+ profiles = config.get("profiles", {})
324
+ for name in sorted_profile_names(profiles):
325
+ profile = profiles[name]
326
+ summary = format_profile_summary(name, profile)
327
+ print(f" {summary}")
328
+ print()
329
+
330
+ return 0
331
+
332
+
333
+ def run_agent(args: argparse.Namespace) -> int:
334
+ config = load_config(args.config if hasattr(args, "config") else None)
335
+ agent = config["agent"].get("default")
336
+ available = detect_available_agents()
337
+
338
+ print("Agent configuration:")
339
+ print(f" Default: {agent or '(not set)'}")
340
+ print()
341
+ if available:
342
+ print("Available agents:")
343
+ for name in sorted(available):
344
+ print(f" ✓ {name}")
345
+ else:
346
+ print("Available agents: (none detected)")
347
+ return 0
348
+
349
+
350
+ def run_validation(args: argparse.Namespace) -> int:
351
+ config = load_config(args.config if hasattr(args, "config") else None)
352
+ validation = config.get("validation", {})
353
+ print("Validation settings:")
354
+ print(f" reference_max_lines: {validation.get('reference_max_lines')}")
355
+ print(f" strict: {validation.get('strict')}")
356
+ return 0
357
+
358
+
359
+ def run_adapter(args: argparse.Namespace) -> int:
360
+ config = load_config(args.config if hasattr(args, "config") else None)
361
+ adapter = config.get("adapter", {})
362
+ targets = adapter.get("default_targets", [])
363
+ print("Adapter settings:")
364
+ print(f" default_targets: {', '.join(targets)}")
365
+ return 0
366
+
367
+
368
+ def run_oasr(args: argparse.Namespace) -> int:
369
+ config = load_config(args.config if hasattr(args, "config") else None)
370
+ oasr = config.get("oasr", {})
371
+ print("OASR settings:")
372
+ print(f" default_profile: {oasr.get('default_profile')}")
373
+ print(f" completions: {_format_value(oasr.get('completions'))}")
205
374
  return 0
206
375
 
207
376
 
377
+ def run_profiles(args: argparse.Namespace) -> int:
378
+ config = load_config(args.config if hasattr(args, "config") else None)
379
+ profiles = config.get("profiles", {})
380
+ names = sorted_profile_names(profiles)
381
+
382
+ if args.names:
383
+ print("\n".join(names))
384
+ return 0
385
+
386
+ print("Profiles:")
387
+ for name in names:
388
+ summary = format_profile_summary(name, profiles[name])
389
+ suffix = " (built-in)" if name in BUILTIN_PROFILES else ""
390
+ print(f" {summary}{suffix}")
391
+ print("\nProfile files: ~/.oasr/profile/*.toml (inline config overrides)")
392
+ return 0
393
+
394
+
395
+ def run_man(args: argparse.Namespace) -> int:
396
+ print("OASR configuration reference")
397
+ print()
398
+ print("Quick reference:")
399
+ print(" oasr config set <key> <value>")
400
+ print(" oasr config get <key>")
401
+ print(" oasr config list")
402
+ print(" oasr config path")
403
+ print(" oasr config validate")
404
+ print()
405
+ print("Common keys:")
406
+ print(" agent")
407
+ print(" oasr.default_profile")
408
+ print(" oasr.completions")
409
+ print(" validation.strict")
410
+ print(" validation.reference_max_lines")
411
+ print(" adapter.default_targets")
412
+ print()
413
+ print("Examples:")
414
+ print(" oasr config set agent codex")
415
+ print(" oasr config set oasr.default_profile dev")
416
+ print(" oasr config set validation.strict true")
417
+ print(" oasr config set adapter.default_targets cursor,windsurf")
418
+ print()
419
+ print("Profiles:")
420
+ print(" - Built-ins: safe, strict, dev, unsafe")
421
+ print(" - Inline: [profiles.<name>] in config.toml")
422
+ print(" - Files: ~/.oasr/profile/<name>.toml (body keys only)")
423
+ print()
424
+ print("Docs: docs/commands/CONFIG.md and docs/configuration/README.md")
425
+ return 0
426
+
427
+
428
+ def run_validate(args: argparse.Namespace) -> int:
429
+ config_path = args.config if hasattr(args, "config") and args.config else CONFIG_FILE
430
+ if not config_path.exists():
431
+ defaults = get_default_config()
432
+ try:
433
+ save_config(defaults, config_path=config_path)
434
+ print(f"✓ Created default config at {config_path}")
435
+ except ValueError as exc:
436
+ print(f"Error: {exc}", file=sys.stderr)
437
+ return 1
438
+
439
+ try:
440
+ file_config = _load_file_config(config_path)
441
+ if not isinstance(file_config, dict):
442
+ raise ValueError("Config file must contain a table")
443
+ inline_profiles = file_config.get("profiles", {})
444
+ _ = load_profiles(inline_profiles=inline_profiles if isinstance(inline_profiles, dict) else {})
445
+ save_config(file_config, config_path=config_path)
446
+ print(f"✓ Config valid: {config_path}")
447
+ if list_profile_files():
448
+ print("✓ Profile files loaded")
449
+ return 0
450
+ except ValueError as exc:
451
+ print(f"Error: {exc}", file=sys.stderr)
452
+ return 1
453
+
454
+
208
455
  def run_path(args: argparse.Namespace) -> int:
209
456
  """Show config file path."""
210
457
  if hasattr(args, "config") and args.config:
commands/exec.py CHANGED
@@ -43,6 +43,11 @@ def setup_parser(subparsers):
43
43
  "--agent",
44
44
  help="Override the default agent (codex, copilot, claude, opencode)",
45
45
  )
46
+ parser.add_argument(
47
+ "--unsafe",
48
+ action="store_true",
49
+ help="Pass unsafe mode flags to the agent CLI (use with caution)",
50
+ )
46
51
  parser.add_argument(
47
52
  "--profile",
48
53
  help="Execution policy profile to use (default: from config)",
@@ -161,7 +166,22 @@ def run(args: argparse.Namespace) -> int:
161
166
  print("━" * 60, file=sys.stderr)
162
167
 
163
168
  try:
164
- result = driver.execute(skill_content, user_prompt)
169
+ extra_args = []
170
+ if getattr(args, "unsafe", False):
171
+ if agent_name == "codex":
172
+ extra_args.append("--skip-git-repo-check")
173
+ elif agent_name == "claude":
174
+ extra_args.append("--dangerously-skip-permissions")
175
+ else:
176
+ print(
177
+ f"Warning: --unsafe is not supported for agent '{agent_name}'.",
178
+ file=sys.stderr,
179
+ )
180
+ print(
181
+ "See agent docs for trusted directory or permission configuration.",
182
+ file=sys.stderr,
183
+ )
184
+ result = driver.execute(skill_content, user_prompt, extra_args=extra_args or None)
165
185
  # CompletedProcess has returncode attribute (0 = success)
166
186
  # Output was already streamed to stdout since capture_output=False
167
187
  return result.returncode
commands/profile.py ADDED
@@ -0,0 +1,84 @@
1
+ """`oasr profile` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import questionary
10
+
11
+ from config import CONFIG_FILE, load_config, save_config
12
+ from profiles import BUILTIN_PROFILES, format_profile_summary, sorted_profile_names
13
+
14
+
15
+ def register(subparsers: argparse._SubParsersAction) -> None:
16
+ parser = subparsers.add_parser(
17
+ "profile",
18
+ help="List or set execution profiles",
19
+ description="List and select execution policy profiles",
20
+ )
21
+ parser.add_argument("name", nargs="?", help="Profile name to set as default")
22
+ parser.set_defaults(func=run)
23
+
24
+
25
+ def _print_profiles(profiles: dict[str, dict[str, object]], current: str) -> None:
26
+ names = sorted_profile_names(profiles)
27
+ print("Profiles:")
28
+ for name in names:
29
+ summary = format_profile_summary(name, profiles[name])
30
+ suffix = []
31
+ if name == current:
32
+ suffix.append("current")
33
+ if name in BUILTIN_PROFILES:
34
+ suffix.append("built-in")
35
+ suffix_text = f" ({', '.join(suffix)})" if suffix else ""
36
+ print(f" {summary}{suffix_text}")
37
+
38
+
39
+ def _select_profile(names: list[str], current: str) -> str | None:
40
+ default_choice = current if current in names else (names[0] if names else None)
41
+ if not default_choice:
42
+ return None
43
+ response = questionary.select(
44
+ "Select a default profile:",
45
+ choices=names,
46
+ default=default_choice,
47
+ ).ask()
48
+ return response
49
+
50
+
51
+ def _set_default_profile(config_path: Path, profile_name: str) -> int:
52
+ config = load_config(config_path=config_path)
53
+ profiles = config.get("profiles", {})
54
+ if profile_name not in profiles:
55
+ print(f"Error: Profile '{profile_name}' not found.", file=sys.stderr)
56
+ return 1
57
+
58
+ config["oasr"]["default_profile"] = profile_name
59
+ save_config(config, config_path=config_path)
60
+ print(f"✓ Default profile set to: {profile_name}")
61
+ return 0
62
+
63
+
64
+ def run(args: argparse.Namespace) -> int:
65
+ config_path = CONFIG_FILE
66
+ config = load_config(config_path=config_path)
67
+ profiles = config.get("profiles", {})
68
+ current = config.get("oasr", {}).get("default_profile", "safe")
69
+
70
+ if args.name:
71
+ return _set_default_profile(config_path, args.name)
72
+
73
+ if not sys.stdout.isatty() or not sys.stdin.isatty():
74
+ _print_profiles(profiles, current)
75
+ print("\nTip: run `oasr profile <name>` to set the default profile.", file=sys.stderr)
76
+ return 0
77
+
78
+ names = sorted_profile_names(profiles)
79
+ choice = _select_profile(names, current)
80
+ if not choice:
81
+ print("No profiles available.", file=sys.stderr)
82
+ return 1
83
+
84
+ return _set_default_profile(config_path, choice)