oasr 0.5.1__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 +17 -3
- agents/claude.py +12 -2
- agents/codex.py +12 -2
- agents/copilot.py +12 -2
- agents/opencode.py +12 -2
- cli.py +8 -2
- commands/completion.py +345 -0
- commands/config.py +284 -37
- commands/exec.py +21 -1
- commands/profile.py +84 -0
- commands/update.py +89 -7
- completions/__init__.py +1 -0
- completions/bash.sh +210 -0
- completions/fish.fish +134 -0
- completions/powershell.ps1 +184 -0
- completions/zsh.sh +285 -0
- config/__init__.py +11 -0
- config/defaults.py +4 -21
- config/schema.py +3 -29
- {oasr-0.5.1.dist-info → oasr-0.6.0.dist-info}/METADATA +51 -21
- {oasr-0.5.1.dist-info → oasr-0.6.0.dist-info}/RECORD +34 -20
- policy/defaults.py +3 -19
- policy/profile.py +5 -7
- profiles/__init__.py +23 -0
- profiles/builtins.py +63 -0
- profiles/loader.py +74 -0
- profiles/paths.py +22 -0
- profiles/registry.py +19 -0
- profiles/summary.py +23 -0
- profiles/validation.py +34 -0
- {oasr-0.5.1.dist-info → oasr-0.6.0.dist-info}/WHEEL +0 -0
- {oasr-0.5.1.dist-info → oasr-0.6.0.dist-info}/entry_points.txt +0 -0
- {oasr-0.5.1.dist-info → oasr-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {oasr-0.5.1.dist-info → oasr-0.6.0.dist-info}/licenses/NOTICE +0 -0
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
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
|
|
124
|
-
|
|
229
|
+
if section not in file_config:
|
|
230
|
+
file_config[section] = {}
|
|
125
231
|
|
|
126
|
-
|
|
232
|
+
file_config[section][field] = value
|
|
127
233
|
|
|
128
234
|
try:
|
|
129
|
-
save_config(
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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)
|
commands/update.py
CHANGED
|
@@ -6,6 +6,8 @@ import argparse
|
|
|
6
6
|
import json
|
|
7
7
|
import subprocess
|
|
8
8
|
import sys
|
|
9
|
+
import urllib.request
|
|
10
|
+
from importlib import metadata
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
|
|
11
13
|
|
|
@@ -215,6 +217,51 @@ def get_stats(repo_path: Path, old_commit: str, new_commit: str) -> dict:
|
|
|
215
217
|
return stats
|
|
216
218
|
|
|
217
219
|
|
|
220
|
+
def get_installed_version(package: str = "oasr") -> str | None:
|
|
221
|
+
"""Get installed package version."""
|
|
222
|
+
try:
|
|
223
|
+
return metadata.version(package)
|
|
224
|
+
except metadata.PackageNotFoundError:
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def get_latest_pypi_version(package: str = "oasr") -> str | None:
|
|
229
|
+
"""Fetch the latest version from PyPI."""
|
|
230
|
+
try:
|
|
231
|
+
with urllib.request.urlopen(f"https://pypi.org/pypi/{package}/json", timeout=5) as response:
|
|
232
|
+
data = json.load(response)
|
|
233
|
+
return data.get("info", {}).get("version")
|
|
234
|
+
except Exception:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def upgrade_from_pypi(package: str = "oasr") -> tuple[bool, str]:
|
|
239
|
+
"""Upgrade ASR using uv or pip."""
|
|
240
|
+
commands = [
|
|
241
|
+
["uv", "pip", "install", "--upgrade", package],
|
|
242
|
+
[sys.executable, "-m", "pip", "install", "--upgrade", package],
|
|
243
|
+
]
|
|
244
|
+
last_error = ""
|
|
245
|
+
|
|
246
|
+
for cmd in commands:
|
|
247
|
+
try:
|
|
248
|
+
result = subprocess.run(
|
|
249
|
+
cmd,
|
|
250
|
+
capture_output=True,
|
|
251
|
+
text=True,
|
|
252
|
+
timeout=60,
|
|
253
|
+
)
|
|
254
|
+
if result.returncode == 0:
|
|
255
|
+
runner = "uv" if cmd[0] == "uv" else "pip"
|
|
256
|
+
return True, f"Updated with {runner}"
|
|
257
|
+
last_error = result.stderr.strip() or result.stdout.strip()
|
|
258
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
259
|
+
last_error = "Update timed out" if isinstance(sys.exc_info()[1], subprocess.TimeoutExpired) else last_error
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
return False, last_error or "Failed to update with pip"
|
|
263
|
+
|
|
264
|
+
|
|
218
265
|
def reinstall_asr(repo_path: Path) -> tuple[bool, str]:
|
|
219
266
|
"""Reinstall ASR using uv or pip.
|
|
220
267
|
|
|
@@ -293,10 +340,29 @@ def run(args: argparse.Namespace) -> int:
|
|
|
293
340
|
|
|
294
341
|
if not repo_path:
|
|
295
342
|
if args.json:
|
|
296
|
-
print(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
343
|
+
print(
|
|
344
|
+
json.dumps(
|
|
345
|
+
{
|
|
346
|
+
"success": False,
|
|
347
|
+
"error": "Could not find ASR git repository",
|
|
348
|
+
"hint": "Install from git or use pip to update",
|
|
349
|
+
}
|
|
350
|
+
)
|
|
351
|
+
)
|
|
352
|
+
return 1
|
|
353
|
+
|
|
354
|
+
print("✗ Could not find ASR git repository", file=sys.stderr)
|
|
355
|
+
print(" This command updates git installs only.", file=sys.stderr)
|
|
356
|
+
print(" To update PyPI installs:", file=sys.stderr)
|
|
357
|
+
print(" pip install --upgrade oasr", file=sys.stderr)
|
|
358
|
+
|
|
359
|
+
latest_version = get_latest_pypi_version()
|
|
360
|
+
installed_version = get_installed_version()
|
|
361
|
+
if latest_version and installed_version and latest_version != installed_version:
|
|
362
|
+
print(
|
|
363
|
+
f"\nUpdate available: {installed_version} → {latest_version}",
|
|
364
|
+
file=sys.stderr,
|
|
365
|
+
)
|
|
300
366
|
return 1
|
|
301
367
|
|
|
302
368
|
if not args.quiet and not args.json:
|
|
@@ -306,8 +372,10 @@ def run(args: argparse.Namespace) -> int:
|
|
|
306
372
|
if not (repo_path / ".git").exists():
|
|
307
373
|
if args.json:
|
|
308
374
|
print(json.dumps({"success": False, "error": "Not a git repository"}))
|
|
309
|
-
|
|
310
|
-
|
|
375
|
+
return 1
|
|
376
|
+
print(f"✗ {repo_path} is not a git repository", file=sys.stderr)
|
|
377
|
+
print(" Use pip to update PyPI installs:", file=sys.stderr)
|
|
378
|
+
print(" pip install --upgrade oasr", file=sys.stderr)
|
|
311
379
|
return 1
|
|
312
380
|
|
|
313
381
|
# Get remote URL
|
|
@@ -348,10 +416,24 @@ def run(args: argparse.Namespace) -> int:
|
|
|
348
416
|
|
|
349
417
|
# Check if already up to date
|
|
350
418
|
if message == "already_up_to_date":
|
|
419
|
+
latest_version = get_latest_pypi_version()
|
|
420
|
+
installed_version = get_installed_version()
|
|
421
|
+
|
|
351
422
|
if args.json:
|
|
352
|
-
|
|
423
|
+
payload = {"success": True, "updated": False, "message": "Already up to date"}
|
|
424
|
+
if latest_version and installed_version:
|
|
425
|
+
payload["installed_version"] = installed_version
|
|
426
|
+
payload["latest_version"] = latest_version
|
|
427
|
+
payload["pypi_update_available"] = latest_version != installed_version
|
|
428
|
+
print(json.dumps(payload))
|
|
353
429
|
else:
|
|
354
430
|
print("✓ Already up to date")
|
|
431
|
+
if latest_version and installed_version and latest_version != installed_version:
|
|
432
|
+
print(
|
|
433
|
+
f"\nPyPI update available: {installed_version} → {latest_version}",
|
|
434
|
+
file=sys.stderr,
|
|
435
|
+
)
|
|
436
|
+
print("Run: pip install --upgrade oasr", file=sys.stderr)
|
|
355
437
|
return 0
|
|
356
438
|
|
|
357
439
|
# Get new commit
|
completions/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shell completion scripts for OASR."""
|