@@ -95,7 +95,11 @@ pub(crate) const GLOBAL_OPTIONS: &str = "Global options";
9595//
9696// **Every** global flag is parseable on **every** subcommand. Commands that
9797// don't use a given flag ignore it silently — e.g. `list --global` parses
98- // fine and the `global` field is unused at runtime.
98+ // fine and the `global` field is unused at runtime. The one exception is
99+ // the path flags (`--cwd`, `--global-prefix`, `--manifest-path`): `main`
100+ // validates them on every project command, used or not
101+ // ([`GlobalArgs::validate_paths`]), because a path that names nothing must
102+ // never read as an empty project.
99103//
100104// (Plain `//` comments: clap turns a doc comment here into the `--help`
101105// description of any subcommand that has none of its own.)
@@ -383,6 +387,76 @@ pub struct GlobalArgs {
383387}
384388
385389impl GlobalArgs {
390+ /// Reject path flags that name nothing: `--cwd` and `--global-prefix`
391+ /// (flag or env) must be existing directories, and a `--manifest-path`
392+ /// other than the default must sit in an existing project directory and
393+ /// must not itself be a directory. `main` maps `Err` to the usage exit
394+ /// (2), the same exit a hosted/vendored `scan` PATH that is not a
395+ /// directory gets.
396+ ///
397+ /// Without this a typo in `--cwd` / `SOCKET_CWD` read as an empty
398+ /// project: `apply`, `list`, `scan`, `get` and the `vendor --check` CI
399+ /// gate all exited 0 having checked nothing.
400+ ///
401+ /// A missing manifest FILE stays legal: hosted and vendored projects
402+ /// have none, and `get` / `scan --mode agent` create it. Only its
403+ /// project directory has to exist — except for the commands that
404+ /// create the manifest (`creates_manifest`: `get`, `scan`), which make
405+ /// a missing directory as they always have; writing it is no vacuous
406+ /// pass.
407+ pub fn validate_paths ( & self , creates_manifest : bool ) -> Result < ( ) , String > {
408+ let not_dir = |flag : & str , env : & str , path : & Path , what : & str | {
409+ format ! ( "{flag} (or {env}) `{}` {what}" , path. display( ) )
410+ } ;
411+ if !self . cwd . is_dir ( ) {
412+ let what = if self . cwd . exists ( ) {
413+ "is not a directory"
414+ } else {
415+ "does not exist"
416+ } ;
417+ return Err ( not_dir ( "--cwd" , "SOCKET_CWD" , & self . cwd , what) ) ;
418+ }
419+ if let Some ( prefix) = & self . global_prefix {
420+ if !prefix. is_dir ( ) {
421+ let what = if prefix. exists ( ) {
422+ "is not a directory"
423+ } else {
424+ "does not exist"
425+ } ;
426+ return Err ( not_dir (
427+ "--global-prefix" ,
428+ "SOCKET_GLOBAL_PREFIX" ,
429+ prefix,
430+ what,
431+ ) ) ;
432+ }
433+ }
434+ if self . manifest_path != DEFAULT_PATCH_MANIFEST_PATH {
435+ let manifest = self . resolved_manifest_path ( ) ;
436+ if manifest. is_dir ( ) {
437+ return Err ( not_dir (
438+ "--manifest-path" ,
439+ "SOCKET_MANIFEST_PATH" ,
440+ & manifest,
441+ "is a directory, not a manifest file" ,
442+ ) ) ;
443+ }
444+ let root = self . project_root ( ) ;
445+ if !creates_manifest && !root. is_dir ( ) {
446+ return Err ( not_dir (
447+ "--manifest-path" ,
448+ "SOCKET_MANIFEST_PATH" ,
449+ & manifest,
450+ & format ! (
451+ "is in a project directory that does not exist ({})" ,
452+ root. display( )
453+ ) ,
454+ ) ) ;
455+ }
456+ }
457+ Ok ( ( ) )
458+ }
459+
386460 /// The crawler options this run's `--cwd` / `--global` /
387461 /// `--global-prefix` select.
388462 pub ( crate ) fn crawler_options ( & self ) -> socket_patch_core:: crawlers:: CrawlerOptions {
0 commit comments