ostruct-cli 0.8.8__py3-none-any.whl → 1.0.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.
- ostruct/cli/__init__.py +3 -15
- ostruct/cli/attachment_processor.py +455 -0
- ostruct/cli/attachment_template_bridge.py +973 -0
- ostruct/cli/cli.py +187 -33
- ostruct/cli/click_options.py +775 -692
- ostruct/cli/code_interpreter.py +195 -12
- ostruct/cli/commands/__init__.py +0 -3
- ostruct/cli/commands/run.py +289 -62
- ostruct/cli/config.py +23 -22
- ostruct/cli/constants.py +89 -0
- ostruct/cli/errors.py +191 -6
- ostruct/cli/explicit_file_processor.py +0 -15
- ostruct/cli/file_info.py +118 -14
- ostruct/cli/file_list.py +82 -1
- ostruct/cli/file_search.py +68 -2
- ostruct/cli/help_json.py +235 -0
- ostruct/cli/mcp_integration.py +13 -16
- ostruct/cli/params.py +217 -0
- ostruct/cli/plan_assembly.py +335 -0
- ostruct/cli/plan_printing.py +385 -0
- ostruct/cli/progress_reporting.py +8 -56
- ostruct/cli/quick_ref_help.py +128 -0
- ostruct/cli/rich_config.py +299 -0
- ostruct/cli/runner.py +397 -190
- ostruct/cli/security/__init__.py +2 -0
- ostruct/cli/security/allowed_checker.py +41 -0
- ostruct/cli/security/normalization.py +13 -9
- ostruct/cli/security/security_manager.py +558 -17
- ostruct/cli/security/types.py +15 -0
- ostruct/cli/template_debug.py +283 -261
- ostruct/cli/template_debug_help.py +233 -142
- ostruct/cli/template_env.py +46 -5
- ostruct/cli/template_filters.py +415 -8
- ostruct/cli/template_processor.py +240 -619
- ostruct/cli/template_rendering.py +49 -73
- ostruct/cli/template_validation.py +2 -1
- ostruct/cli/token_validation.py +35 -15
- ostruct/cli/types.py +15 -19
- ostruct/cli/unicode_compat.py +283 -0
- ostruct/cli/upload_manager.py +448 -0
- ostruct/cli/utils.py +30 -0
- ostruct/cli/validators.py +272 -54
- {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/METADATA +292 -126
- ostruct_cli-1.0.0.dist-info/RECORD +80 -0
- ostruct/cli/commands/quick_ref.py +0 -54
- ostruct/cli/template_optimizer.py +0 -478
- ostruct_cli-0.8.8.dist-info/RECORD +0 -71
- {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/WHEEL +0 -0
- {ostruct_cli-0.8.8.dist-info → ostruct_cli-1.0.0.dist-info}/entry_points.txt +0 -0
ostruct/cli/validators.py
CHANGED
@@ -9,7 +9,9 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|
9
9
|
import click
|
10
10
|
import jinja2
|
11
11
|
|
12
|
+
from .constants import DefaultPaths
|
12
13
|
from .errors import (
|
14
|
+
CLIError,
|
13
15
|
DirectoryNotFoundError,
|
14
16
|
InvalidJSONError,
|
15
17
|
SchemaFileError,
|
@@ -17,7 +19,7 @@ from .errors import (
|
|
17
19
|
VariableNameError,
|
18
20
|
VariableValueError,
|
19
21
|
)
|
20
|
-
from .
|
22
|
+
from .exit_codes import ExitCode
|
21
23
|
from .security import SecurityManager
|
22
24
|
from .template_env import create_jinja_env
|
23
25
|
from .template_processor import (
|
@@ -26,6 +28,7 @@ from .template_processor import (
|
|
26
28
|
)
|
27
29
|
from .template_utils import validate_json_schema
|
28
30
|
from .types import CLIParams
|
31
|
+
from .utils import fix_surrogate_escapes
|
29
32
|
|
30
33
|
logger = logging.getLogger(__name__)
|
31
34
|
|
@@ -33,35 +36,6 @@ logger = logging.getLogger(__name__)
|
|
33
36
|
FileRoutingResult = List[Tuple[Optional[str], Union[str, Path]]]
|
34
37
|
|
35
38
|
|
36
|
-
def validate_name_path_pair(
|
37
|
-
ctx: click.Context,
|
38
|
-
param: click.Parameter,
|
39
|
-
value: List[Tuple[str, Union[str, Path]]],
|
40
|
-
) -> List[Tuple[str, Union[str, Path]]]:
|
41
|
-
"""Validate name/path pairs for files and directories.
|
42
|
-
|
43
|
-
Args:
|
44
|
-
ctx: Click context
|
45
|
-
param: Click parameter
|
46
|
-
value: List of (name, path) tuples
|
47
|
-
|
48
|
-
Returns:
|
49
|
-
List of validated (name, Path) tuples
|
50
|
-
|
51
|
-
Raises:
|
52
|
-
click.BadParameter: If validation fails
|
53
|
-
"""
|
54
|
-
if not value:
|
55
|
-
return value
|
56
|
-
|
57
|
-
result: List[Tuple[str, Union[str, Path]]] = []
|
58
|
-
for name, path in value:
|
59
|
-
if not name.isidentifier():
|
60
|
-
raise click.BadParameter(f"Invalid variable name: {name}")
|
61
|
-
result.append((name, Path(path)))
|
62
|
-
return result
|
63
|
-
|
64
|
-
|
65
39
|
def validate_file_routing_spec(
|
66
40
|
ctx: click.Context,
|
67
41
|
param: click.Parameter,
|
@@ -69,23 +43,23 @@ def validate_file_routing_spec(
|
|
69
43
|
) -> FileRoutingResult:
|
70
44
|
"""Validate file routing specifications supporting multiple syntaxes.
|
71
45
|
|
72
|
-
|
73
|
-
|
74
|
-
- Equals syntax:
|
46
|
+
Supports two syntaxes currently:
|
47
|
+
- Simple path: --file config.yaml (auto-generates variable name)
|
48
|
+
- Equals syntax: --file code_file=src/main.py (custom variable name)
|
75
49
|
|
76
|
-
Note: Two-argument syntax (
|
77
|
-
|
50
|
+
Note: Two-argument syntax (--file name path) requires special handling at the CLI level
|
51
|
+
and is not supported in this validator due to Click's argument processing.
|
78
52
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
53
|
+
Args:
|
54
|
+
ctx: Click context
|
55
|
+
param: Click parameter
|
56
|
+
value: List of file specifications from multiple options
|
83
57
|
|
84
|
-
|
85
|
-
|
58
|
+
Returns:
|
59
|
+
List of (variable_name, path) tuples where variable_name=None means auto-generate
|
86
60
|
|
87
|
-
|
88
|
-
|
61
|
+
Raises:
|
62
|
+
click.BadParameter: If validation fails
|
89
63
|
"""
|
90
64
|
if not value:
|
91
65
|
return []
|
@@ -94,7 +68,7 @@ def validate_file_routing_spec(
|
|
94
68
|
|
95
69
|
for spec in value:
|
96
70
|
if "=" in spec:
|
97
|
-
# Equals syntax:
|
71
|
+
# Equals syntax: --file code_file=src/main.py
|
98
72
|
if spec.count("=") != 1:
|
99
73
|
raise click.BadParameter(
|
100
74
|
f"Invalid format '{spec}'. Use name=path or just path."
|
@@ -112,7 +86,7 @@ def validate_file_routing_spec(
|
|
112
86
|
raise click.BadParameter(f"File not found: {path}")
|
113
87
|
result.append((name, Path(path)))
|
114
88
|
else:
|
115
|
-
# Simple path:
|
89
|
+
# Simple path: --file config.yaml
|
116
90
|
path = spec.strip()
|
117
91
|
if not path:
|
118
92
|
raise click.BadParameter("Empty file path")
|
@@ -145,6 +119,9 @@ def validate_variable(
|
|
145
119
|
|
146
120
|
result = []
|
147
121
|
for var in value:
|
122
|
+
# Fix any surrogate escape issues in the variable string
|
123
|
+
var = fix_surrogate_escapes(var)
|
124
|
+
|
148
125
|
if "=" not in var:
|
149
126
|
raise click.BadParameter(
|
150
127
|
f"Variable must be in format name=value: {var}"
|
@@ -152,6 +129,11 @@ def validate_variable(
|
|
152
129
|
name, val = var.split("=", 1)
|
153
130
|
name = name.strip()
|
154
131
|
val = val.strip()
|
132
|
+
|
133
|
+
# Fix surrogate escapes in both name and value
|
134
|
+
name = fix_surrogate_escapes(name)
|
135
|
+
val = fix_surrogate_escapes(val)
|
136
|
+
|
155
137
|
if not name.isidentifier():
|
156
138
|
raise click.BadParameter(f"Invalid variable name: {name}")
|
157
139
|
result.append((name, val))
|
@@ -179,13 +161,21 @@ def validate_json_variable(
|
|
179
161
|
|
180
162
|
result = []
|
181
163
|
for var in value:
|
164
|
+
# Fix any surrogate escape issues in the variable string
|
165
|
+
var = fix_surrogate_escapes(var)
|
166
|
+
|
182
167
|
if "=" not in var:
|
183
168
|
raise InvalidJSONError(
|
184
|
-
f
|
169
|
+
f'JSON variable must be in format name={{"json":"value"}}: {var}'
|
185
170
|
)
|
186
171
|
name, json_str = var.split("=", 1)
|
187
172
|
name = name.strip()
|
188
173
|
json_str = json_str.strip()
|
174
|
+
|
175
|
+
# Fix surrogate escapes in both name and JSON string
|
176
|
+
name = fix_surrogate_escapes(name)
|
177
|
+
json_str = fix_surrogate_escapes(json_str)
|
178
|
+
|
189
179
|
if not name.isidentifier():
|
190
180
|
raise VariableNameError(f"Invalid variable name: {name}")
|
191
181
|
try:
|
@@ -372,6 +362,9 @@ def validate_security_manager(
|
|
372
362
|
base_dir: Optional[str] = None,
|
373
363
|
allowed_dirs: Optional[List[str]] = None,
|
374
364
|
allowed_dir_file: Optional[str] = None,
|
365
|
+
allow_files: Optional[List[str]] = None,
|
366
|
+
allow_lists: Optional[List[str]] = None,
|
367
|
+
security_mode: str = "warn",
|
375
368
|
) -> SecurityManager:
|
376
369
|
"""Validate and create security manager.
|
377
370
|
|
@@ -379,6 +372,9 @@ def validate_security_manager(
|
|
379
372
|
base_dir: Base directory for file access. Defaults to current working directory.
|
380
373
|
allowed_dirs: Optional list of additional allowed directories
|
381
374
|
allowed_dir_file: Optional file containing allowed directories
|
375
|
+
allow_files: Optional list of individual files to allow (tracked by inode)
|
376
|
+
allow_lists: Optional list of allow-list files to load
|
377
|
+
security_mode: Path security mode (permissive/warn/strict)
|
382
378
|
|
383
379
|
Returns:
|
384
380
|
Configured SecurityManager instance
|
@@ -391,8 +387,22 @@ def validate_security_manager(
|
|
391
387
|
if base_dir is None:
|
392
388
|
base_dir = os.getcwd()
|
393
389
|
|
394
|
-
#
|
395
|
-
|
390
|
+
# Parse security mode
|
391
|
+
from .security.types import PathSecurity
|
392
|
+
|
393
|
+
# Ensure security_mode is not None (safety check)
|
394
|
+
if security_mode is None:
|
395
|
+
security_mode = "warn"
|
396
|
+
|
397
|
+
try:
|
398
|
+
parsed_security_mode = PathSecurity(security_mode.lower())
|
399
|
+
except ValueError:
|
400
|
+
parsed_security_mode = PathSecurity.WARN
|
401
|
+
|
402
|
+
# Create security manager with base directory and security mode
|
403
|
+
security_manager = SecurityManager(
|
404
|
+
base_dir, security_mode=parsed_security_mode
|
405
|
+
)
|
396
406
|
|
397
407
|
# Add explicitly allowed directories
|
398
408
|
if allowed_dirs:
|
@@ -412,9 +422,114 @@ def validate_security_manager(
|
|
412
422
|
f"Failed to read allowed directories file: {e}"
|
413
423
|
)
|
414
424
|
|
425
|
+
# Configure enhanced security features (allow_files and allow_lists)
|
426
|
+
if allow_files or allow_lists:
|
427
|
+
security_manager.configure_security_mode(
|
428
|
+
mode=parsed_security_mode,
|
429
|
+
allow_files=allow_files,
|
430
|
+
allow_lists=allow_lists,
|
431
|
+
)
|
432
|
+
|
415
433
|
return security_manager
|
416
434
|
|
417
435
|
|
436
|
+
def validate_download_configuration(args: CLIParams) -> None:
|
437
|
+
"""Validate download configuration without creating directories.
|
438
|
+
|
439
|
+
This function validates that the download directory can be written to
|
440
|
+
when Code Interpreter is being used, without actually creating the directory.
|
441
|
+
|
442
|
+
Args:
|
443
|
+
args: CLI parameters
|
444
|
+
|
445
|
+
Raises:
|
446
|
+
CLIError: If download directory validation fails
|
447
|
+
"""
|
448
|
+
logger.debug("=== Download Directory Validation ===")
|
449
|
+
|
450
|
+
# Check if Code Interpreter is being used
|
451
|
+
routing_result = args.get("_routing_result")
|
452
|
+
logger.debug(f"Routing result: {routing_result}")
|
453
|
+
|
454
|
+
if not routing_result or not hasattr(routing_result, "enabled_tools"):
|
455
|
+
logger.debug(
|
456
|
+
"No routing result available, skipping download validation"
|
457
|
+
)
|
458
|
+
return # No routing result available, skip validation
|
459
|
+
|
460
|
+
logger.debug(f"Enabled tools: {routing_result.enabled_tools}")
|
461
|
+
|
462
|
+
if "code-interpreter" not in routing_result.enabled_tools:
|
463
|
+
logger.debug(
|
464
|
+
"Code Interpreter not being used, skipping download validation"
|
465
|
+
)
|
466
|
+
return # Code Interpreter not being used, skip validation
|
467
|
+
|
468
|
+
# Get resolved download directory using same logic as runner.py
|
469
|
+
from typing import Union, cast
|
470
|
+
|
471
|
+
from .config import OstructConfig
|
472
|
+
|
473
|
+
config_path = cast(Union[str, Path, None], args.get("config"))
|
474
|
+
config = OstructConfig.load(config_path)
|
475
|
+
ci_config = config.get_code_interpreter_config()
|
476
|
+
|
477
|
+
download_dir = (
|
478
|
+
args.get("ci_download_dir") # CLI flag has highest priority
|
479
|
+
or ci_config.get("output_directory") # Config file is second
|
480
|
+
or DefaultPaths.CODE_INTERPRETER_OUTPUT_DIR # Default is last
|
481
|
+
)
|
482
|
+
|
483
|
+
logger.debug(f"Validating download directory: {download_dir}")
|
484
|
+
|
485
|
+
# Convert to Path object
|
486
|
+
download_path = Path(download_dir)
|
487
|
+
|
488
|
+
# Validate parent directory exists and is writable
|
489
|
+
parent_dir = download_path.parent
|
490
|
+
if not parent_dir.exists():
|
491
|
+
raise CLIError(
|
492
|
+
f"Parent directory does not exist: {parent_dir}\n"
|
493
|
+
f"💡 Try:\n"
|
494
|
+
f" • Create parent directory: mkdir -p {parent_dir}\n"
|
495
|
+
f" • Use different directory: --ci-download-dir ./my-downloads\n"
|
496
|
+
f" • Check current directory: pwd",
|
497
|
+
exit_code=ExitCode.USAGE_ERROR,
|
498
|
+
)
|
499
|
+
|
500
|
+
if not os.access(parent_dir, os.W_OK):
|
501
|
+
raise CLIError(
|
502
|
+
f"No write permission to parent directory: {parent_dir}\n"
|
503
|
+
f"💡 Try:\n"
|
504
|
+
f" • Check directory permissions: ls -la {parent_dir}\n"
|
505
|
+
f" • Fix permissions: chmod u+w {parent_dir}\n"
|
506
|
+
f" • Use different directory: --ci-download-dir ./my-downloads",
|
507
|
+
exit_code=ExitCode.USAGE_ERROR,
|
508
|
+
)
|
509
|
+
|
510
|
+
# Check if download directory exists and is writable (if it exists)
|
511
|
+
if download_path.exists():
|
512
|
+
if not download_path.is_dir():
|
513
|
+
raise CLIError(
|
514
|
+
f"Download path exists but is not a directory: {download_path}\n"
|
515
|
+
f"💡 Try:\n"
|
516
|
+
f" • Remove the file: rm {download_path}\n"
|
517
|
+
f" • Use different directory: --ci-download-dir ./my-downloads",
|
518
|
+
exit_code=ExitCode.USAGE_ERROR,
|
519
|
+
)
|
520
|
+
|
521
|
+
if not os.access(download_path, os.W_OK):
|
522
|
+
raise CLIError(
|
523
|
+
f"No write permission to download directory: {download_path}\n"
|
524
|
+
f"💡 Try:\n"
|
525
|
+
f" • Fix permissions: chmod u+w {download_path}\n"
|
526
|
+
f" • Use different directory: --ci-download-dir ./my-downloads",
|
527
|
+
exit_code=ExitCode.USAGE_ERROR,
|
528
|
+
)
|
529
|
+
|
530
|
+
logger.debug(f"Download directory validation passed: {download_dir}")
|
531
|
+
|
532
|
+
|
418
533
|
async def validate_inputs(
|
419
534
|
args: CLIParams,
|
420
535
|
) -> Tuple[
|
@@ -444,16 +559,37 @@ async def validate_inputs(
|
|
444
559
|
SchemaValidationError: When schema is invalid
|
445
560
|
"""
|
446
561
|
logger.debug("=== Input Validation Phase ===")
|
562
|
+
logger.debug(f"validate_inputs called with args keys: {list(args.keys())}")
|
447
563
|
security_manager = validate_security_manager(
|
448
564
|
base_dir=args.get("base_dir"),
|
449
|
-
allowed_dirs=args.get("
|
565
|
+
allowed_dirs=args.get("allow_dir"), # type: ignore[arg-type]
|
450
566
|
allowed_dir_file=args.get("allowed_dir_file"),
|
567
|
+
allow_files=args.get("allow_file"), # type: ignore[arg-type]
|
568
|
+
allow_lists=args.get("allow_list"), # type: ignore[arg-type]
|
569
|
+
security_mode=args.get("path_security", "warn"), # type: ignore[arg-type]
|
451
570
|
)
|
452
571
|
|
453
|
-
# Process
|
454
|
-
logger.debug("Processing
|
455
|
-
|
456
|
-
|
572
|
+
# Process file routing using new attachment system only (T3.0)
|
573
|
+
logger.debug("Processing file routing with new attachment system")
|
574
|
+
|
575
|
+
from .attachment_processor import process_new_attachments
|
576
|
+
|
577
|
+
routing_result = process_new_attachments(args, security_manager)
|
578
|
+
|
579
|
+
# The new system is mandatory - no fallback to legacy file routing
|
580
|
+
if routing_result is None:
|
581
|
+
# Create empty routing result if no attachments specified
|
582
|
+
from .explicit_file_processor import ExplicitRouting, ProcessingResult
|
583
|
+
|
584
|
+
routing_result = ProcessingResult(
|
585
|
+
routing=ExplicitRouting(),
|
586
|
+
enabled_tools=set(),
|
587
|
+
validated_files={},
|
588
|
+
auto_enabled_feedback=None,
|
589
|
+
)
|
590
|
+
logger.debug("No attachments specified - created empty routing result")
|
591
|
+
else:
|
592
|
+
logger.debug("Processed new attachment system")
|
457
593
|
|
458
594
|
# Display auto-enablement feedback to user
|
459
595
|
if routing_result.auto_enabled_feedback:
|
@@ -462,6 +598,11 @@ async def validate_inputs(
|
|
462
598
|
# Store routing result in args for use by tool processors
|
463
599
|
args["_routing_result"] = routing_result # type: ignore[typeddict-unknown-key]
|
464
600
|
|
601
|
+
# Validate download directory configuration (after routing is processed)
|
602
|
+
logger.debug("About to call validate_download_configuration")
|
603
|
+
validate_download_configuration(args)
|
604
|
+
logger.debug("Finished validate_download_configuration")
|
605
|
+
|
465
606
|
task_template = validate_task_template(
|
466
607
|
args.get("task"), args.get("task_file")
|
467
608
|
)
|
@@ -484,7 +625,27 @@ async def validate_inputs(
|
|
484
625
|
template_context = await create_template_context_from_routing(
|
485
626
|
args, security_manager, routing_result
|
486
627
|
)
|
487
|
-
|
628
|
+
|
629
|
+
# Extract files from template context for file reference support
|
630
|
+
files = []
|
631
|
+
for key, value in template_context.items():
|
632
|
+
if hasattr(value, "__iter__") and not isinstance(value, (str, dict)):
|
633
|
+
# Check if this is a list of FileInfo objects
|
634
|
+
try:
|
635
|
+
for item in value:
|
636
|
+
if hasattr(
|
637
|
+
item, "parent_alias"
|
638
|
+
): # FileInfo with TSES fields
|
639
|
+
files.append(item)
|
640
|
+
except (TypeError, AttributeError):
|
641
|
+
continue
|
642
|
+
|
643
|
+
# Create environment with file reference support
|
644
|
+
env, alias_manager = create_jinja_env(files=files)
|
645
|
+
|
646
|
+
# Store alias manager in args for use by template processor if it has any aliases
|
647
|
+
if alias_manager.aliases:
|
648
|
+
args["_alias_manager"] = alias_manager # type: ignore[typeddict-unknown-key]
|
488
649
|
|
489
650
|
return (
|
490
651
|
security_manager,
|
@@ -494,3 +655,60 @@ async def validate_inputs(
|
|
494
655
|
env,
|
495
656
|
args.get("task_file"),
|
496
657
|
)
|
658
|
+
|
659
|
+
|
660
|
+
def parse_file_path_spec(
|
661
|
+
ctx: click.Context,
|
662
|
+
param: click.Parameter,
|
663
|
+
value: str,
|
664
|
+
) -> Optional[Tuple[Optional[str], str]]:
|
665
|
+
"""Parse file path specification with optional alias.
|
666
|
+
|
667
|
+
Supports multiple formats:
|
668
|
+
- Simple path: --file config.yaml (auto-generates variable name)
|
669
|
+
- Equals syntax: --file code_file=src/main.py (custom variable name)
|
670
|
+
|
671
|
+
Note: Two-argument syntax (--file name path) requires special handling at the CLI level
|
672
|
+
and is not processed by this validator.
|
673
|
+
|
674
|
+
Args:
|
675
|
+
ctx: Click context
|
676
|
+
param: Click parameter
|
677
|
+
value: Raw value from CLI
|
678
|
+
|
679
|
+
Returns:
|
680
|
+
Tuple of (alias, path) or None if value is None
|
681
|
+
|
682
|
+
Raises:
|
683
|
+
click.BadParameter: If the format is invalid
|
684
|
+
"""
|
685
|
+
if not value:
|
686
|
+
return None
|
687
|
+
|
688
|
+
if "=" in value:
|
689
|
+
# Equals syntax: --file code_file=src/main.py
|
690
|
+
if value.count("=") != 1:
|
691
|
+
raise click.BadParameter(
|
692
|
+
f"Invalid format '{value}'. Use name=path or just path."
|
693
|
+
)
|
694
|
+
name, path = value.split("=", 1)
|
695
|
+
if not name.strip():
|
696
|
+
raise click.BadParameter(f"Empty variable name in '{value}'")
|
697
|
+
if not path.strip():
|
698
|
+
raise click.BadParameter(f"Empty path in '{value}'")
|
699
|
+
name = name.strip()
|
700
|
+
path = path.strip()
|
701
|
+
if not name.isidentifier():
|
702
|
+
raise click.BadParameter(f"Invalid variable name: {name}")
|
703
|
+
if not Path(path).exists():
|
704
|
+
raise click.BadParameter(f"File not found: {path}")
|
705
|
+
return (name, path)
|
706
|
+
else:
|
707
|
+
# Simple path: --file config.yaml
|
708
|
+
path = value.strip()
|
709
|
+
if not path:
|
710
|
+
raise click.BadParameter("Empty file path")
|
711
|
+
if not Path(path).exists():
|
712
|
+
raise click.BadParameter(f"File not found: {path}")
|
713
|
+
# Mark as auto-name with None, will be processed later
|
714
|
+
return (None, path)
|