@@ -373,12 +373,14 @@ pub async fn fetch_scopes_for_apis(enabled_api_ids: &[String]) -> Vec<Discovered
373373pub struct SetupOptions {
374374 pub project : Option < String > ,
375375 pub dry_run : bool ,
376+ pub login : bool ,
376377}
377378
378379/// Parse setup flags from args.
379380pub fn parse_setup_args ( args : & [ String ] ) -> SetupOptions {
380381 let mut project = None ;
381382 let mut dry_run = false ;
383+ let mut login = false ;
382384 let mut i = 0 ;
383385 while i < args. len ( ) {
384386 if args[ i] == "--project" && i + 1 < args. len ( ) {
@@ -390,11 +392,18 @@ pub fn parse_setup_args(args: &[String]) -> SetupOptions {
390392 } else if args[ i] == "--dry-run" {
391393 dry_run = true ;
392394 i += 1 ;
395+ } else if args[ i] == "--login" {
396+ login = true ;
397+ i += 1 ;
393398 } else {
394399 i += 1 ;
395400 }
396401 }
397- SetupOptions { project, dry_run }
402+ SetupOptions {
403+ project,
404+ dry_run,
405+ login,
406+ }
398407}
399408
400409// ── gcloud helpers ──────────────────────────────────────────────
@@ -637,7 +646,6 @@ fn is_tos_precondition_error(gcloud_output: &str) -> bool {
637646fn is_invalid_project_id_error ( gcloud_output : & str ) -> bool {
638647 let lower = gcloud_output. to_ascii_lowercase ( ) ;
639648 lower. contains ( "argument project_id: bad value" )
640- || lower. contains ( "project ids are immutable" )
641649 || lower. contains ( "project ids must be between 6 and 30 characters" )
642650}
643651
@@ -646,6 +654,7 @@ fn is_project_id_in_use_error(gcloud_output: &str) -> bool {
646654 lower. contains ( "already in use" )
647655 || lower. contains ( "already exists" )
648656 || lower. contains ( "already being used" )
657+ || lower. contains ( "project ids are immutable" )
649658}
650659
651660fn primary_gcloud_error_line ( gcloud_output : & str ) -> Option < String > {
@@ -1555,6 +1564,38 @@ async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result<SetupStage, Gws
15551564 Ok ( SetupStage :: Finish )
15561565}
15571566
1567+ fn should_offer_login_prompt (
1568+ interactive : bool ,
1569+ dry_run : bool ,
1570+ login_requested : bool ,
1571+ stdout_is_terminal : bool ,
1572+ ) -> bool {
1573+ interactive && !dry_run && !login_requested && stdout_is_terminal
1574+ }
1575+
1576+ fn prompt_login_after_setup ( ) -> Result < bool , GwsError > {
1577+ use std:: io:: Write ;
1578+
1579+ let mut input = String :: new ( ) ;
1580+ loop {
1581+ eprint ! ( "Run `gws auth login` now? [Y/n]: " ) ;
1582+ std:: io:: stderr ( )
1583+ . flush ( )
1584+ . map_err ( |e| GwsError :: Validation ( format ! ( "Failed to flush prompt: {e}" ) ) ) ?;
1585+
1586+ input. clear ( ) ;
1587+ std:: io:: stdin ( )
1588+ . read_line ( & mut input)
1589+ . map_err ( |e| GwsError :: Validation ( format ! ( "Failed to read prompt input: {e}" ) ) ) ?;
1590+
1591+ match input. trim ( ) . to_ascii_lowercase ( ) . as_str ( ) {
1592+ "" | "y" | "yes" => return Ok ( true ) ,
1593+ "n" | "no" => return Ok ( false ) ,
1594+ _ => eprintln ! ( "Please answer 'y' or 'n'." ) ,
1595+ }
1596+ }
1597+ }
1598+
15581599/// Run the full setup flow. Orchestrates all steps and outputs JSON summary.
15591600pub async fn run_setup ( args : & [ String ] ) -> Result < ( ) , GwsError > {
15601601 let opts = parse_setup_args ( args) ;
@@ -1604,9 +1645,28 @@ pub async fn run_setup(args: &[String]) -> Result<(), GwsError> {
16041645
16051646 ctx. finish_wizard ( ) ;
16061647
1648+ let run_login = if ctx. opts . login {
1649+ true
1650+ } else if should_offer_login_prompt (
1651+ ctx. interactive ,
1652+ ctx. dry_run ,
1653+ ctx. opts . login ,
1654+ std:: io:: IsTerminal :: is_terminal ( & std:: io:: stdout ( ) ) ,
1655+ ) {
1656+ prompt_login_after_setup ( ) ?
1657+ } else {
1658+ false
1659+ } ;
1660+
1661+ let message = if run_login {
1662+ "Setup complete! Starting `gws auth login`..."
1663+ } else {
1664+ "Setup complete! Run `gws auth login` to authenticate."
1665+ } ;
1666+
16071667 let output = json ! ( {
16081668 "status" : "success" ,
1609- "message" : "Setup complete! Run `gws auth login` to authenticate." ,
1669+ "message" : message ,
16101670 "account" : ctx. account,
16111671 "project" : ctx. project_id,
16121672 "apis_enabled" : ctx. enabled. len( ) ,
@@ -1619,7 +1679,11 @@ pub async fn run_setup(args: &[String]) -> Result<(), GwsError> {
16191679 serde_json:: to_string_pretty( & output) . unwrap_or_default( )
16201680 ) ;
16211681
1622- eprintln ! ( "\n ✅ Setup complete! Run `gws auth login` to authenticate." ) ;
1682+ eprintln ! ( "\n ✅ {message}" ) ;
1683+
1684+ if run_login {
1685+ crate :: auth_commands:: run_login ( & [ ] ) . await ?;
1686+ }
16231687
16241688 Ok ( ( ) )
16251689}
@@ -1764,34 +1828,39 @@ mod tests {
17641828 let opts = parse_setup_args ( & [ ] ) ;
17651829 assert ! ( opts. project. is_none( ) ) ;
17661830 assert ! ( !opts. dry_run) ;
1831+ assert ! ( !opts. login) ;
17671832 }
17681833
17691834 #[ test]
17701835 fn test_parse_setup_args_with_project ( ) {
17711836 let args = vec ! [ "--project" . into( ) , "my-project" . into( ) ] ;
17721837 let opts = parse_setup_args ( & args) ;
17731838 assert_eq ! ( opts. project. as_deref( ) , Some ( "my-project" ) ) ;
1839+ assert ! ( !opts. login) ;
17741840 }
17751841
17761842 #[ test]
17771843 fn test_parse_setup_args_with_project_equals ( ) {
17781844 let args = vec ! [ "--project=my-project" . into( ) ] ;
17791845 let opts = parse_setup_args ( & args) ;
17801846 assert_eq ! ( opts. project. as_deref( ) , Some ( "my-project" ) ) ;
1847+ assert ! ( !opts. login) ;
17811848 }
17821849
17831850 #[ test]
17841851 fn test_parse_setup_args_ignores_unknown ( ) {
17851852 let args = vec ! [ "--verbose" . into( ) , "--unknown" . into( ) ] ;
17861853 let opts = parse_setup_args ( & args) ;
17871854 assert ! ( opts. project. is_none( ) ) ;
1855+ assert ! ( !opts. login) ;
17881856 }
17891857
17901858 #[ test]
17911859 fn test_parse_setup_args_dry_run ( ) {
17921860 let args = vec ! [ "--dry-run" . into( ) ] ;
17931861 let opts = parse_setup_args ( & args) ;
17941862 assert ! ( opts. dry_run) ;
1863+ assert ! ( !opts. login) ;
17951864 }
17961865
17971866 #[ test]
@@ -1800,6 +1869,36 @@ mod tests {
18001869 let opts = parse_setup_args ( & args) ;
18011870 assert ! ( opts. dry_run) ;
18021871 assert_eq ! ( opts. project. as_deref( ) , Some ( "p" ) ) ;
1872+ assert ! ( !opts. login) ;
1873+ }
1874+
1875+ #[ test]
1876+ fn test_parse_setup_args_login_flag ( ) {
1877+ let args: Vec < String > = vec ! [ "--login" . into( ) ] ;
1878+ let opts = parse_setup_args ( & args) ;
1879+ assert ! ( opts. login) ;
1880+ assert ! ( !opts. dry_run) ;
1881+ assert ! ( opts. project. is_none( ) ) ;
1882+ }
1883+
1884+ #[ test]
1885+ fn test_should_offer_login_prompt_default_interactive ( ) {
1886+ assert ! ( should_offer_login_prompt( true , false , false , true ) ) ;
1887+ }
1888+
1889+ #[ test]
1890+ fn test_should_not_offer_login_prompt_when_login_requested ( ) {
1891+ assert ! ( !should_offer_login_prompt( true , false , true , true ) ) ;
1892+ }
1893+
1894+ #[ test]
1895+ fn test_should_not_offer_login_prompt_non_interactive ( ) {
1896+ assert ! ( !should_offer_login_prompt( false , false , false , true ) ) ;
1897+ }
1898+
1899+ #[ test]
1900+ fn test_should_not_offer_login_prompt_dry_run ( ) {
1901+ assert ! ( !should_offer_login_prompt( true , true , false , true ) ) ;
18031902 }
18041903
18051904 #[ test]
@@ -1843,6 +1942,17 @@ mod tests {
18431942 assert ! ( msg. contains( "different unique project ID" ) ) ;
18441943 }
18451944
1945+ #[ test]
1946+ fn test_format_project_create_failure_immutable_guidance ( ) {
1947+ let msg = format_project_create_failure (
1948+ "example-project-123456" ,
1949+ "" ,
1950+ "Project IDs are immutable and can be set only during project creation." ,
1951+ ) ;
1952+
1953+ assert ! ( msg. contains( "ID is already in use" ) ) ;
1954+ }
1955+
18461956 // ── Account selection → gcloud action ───────────────────────
18471957
18481958 #[ test]
0 commit comments