From 9ed49eb19f9e968203f7bc88bdfae52e84f299b1 Mon Sep 17 00:00:00 2001 From: viraj Date: Sat, 14 Feb 2026 14:06:47 -0500 Subject: [PATCH] feat: prompt to initialize Astro project when running dev commands outside project dir Instead of immediately erroring when astro dev commands (start, stop, restart, etc.) are run outside an Astro project directory, prompt the user to initialize one in-place. Co-Authored-By: Claude Sonnet 4.5 --- cmd/airflow_hooks.go | 33 ++++++++++-- cmd/airflow_hooks_test.go | 103 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 cmd/airflow_hooks_test.go diff --git a/cmd/airflow_hooks.go b/cmd/airflow_hooks.go index 9649e440e..b7feeb69e 100644 --- a/cmd/airflow_hooks.go +++ b/cmd/airflow_hooks.go @@ -6,8 +6,10 @@ import ( "path/filepath" "github.com/astronomer/astro-cli/airflow/runtimes" - "github.com/astronomer/astro-cli/cmd/utils" "github.com/astronomer/astro-cli/config" + "github.com/astronomer/astro-cli/pkg/ansi" + "github.com/astronomer/astro-cli/pkg/input" + "github.com/pkg/errors" "github.com/spf13/cobra" ) @@ -28,7 +30,7 @@ func ConfigureContainerRuntime(_ *cobra.Command, _ []string) error { // EnsureRuntime is a pre-run hook that ensures that the project directory exists // and starts the container runtime if necessary. func EnsureRuntime(cmd *cobra.Command, args []string) error { - if err := utils.EnsureProjectDir(cmd, args); err != nil { + if err := EnsureProjectDirOrInit(cmd, args); err != nil { return err } @@ -50,7 +52,7 @@ func EnsureRuntime(cmd *cobra.Command, args []string) error { // SetRuntimeIfExists is a pre-run hook that ensures the project directory exists // and sets the container runtime if its running, otherwise we bail with an error message. func SetRuntimeIfExists(cmd *cobra.Command, args []string) error { - if err := utils.EnsureProjectDir(cmd, args); err != nil { + if err := EnsureProjectDirOrInit(cmd, args); err != nil { return err } return containerRuntime.Configure() @@ -59,7 +61,7 @@ func SetRuntimeIfExists(cmd *cobra.Command, args []string) error { // KillPreRunHook sets the container runtime if its running, // otherwise we bail with an error message. func KillPreRunHook(cmd *cobra.Command, args []string) error { - if err := utils.EnsureProjectDir(cmd, args); err != nil { + if err := EnsureProjectDirOrInit(cmd, args); err != nil { return err } return containerRuntime.ConfigureOrKill() @@ -71,3 +73,26 @@ func KillPostRunHook(_ *cobra.Command, _ []string) error { // Kill the runtime. return containerRuntime.Kill() } + +// EnsureProjectDirOrInit checks if the current directory is an Astro project. +// If it is not, it prompts the user to initialize one. If the user confirms, +// it runs astro dev init in the current directory. +func EnsureProjectDirOrInit(cmd *cobra.Command, args []string) error { + isProjectDir, err := config.IsProjectDir(config.WorkingPath) + if err != nil { + return errors.Wrap(err, ansi.Red("failed to verify that your working directory is an Astro project.\nTry running astro dev init to turn your working directory into an Astro project")) + } + + if !isProjectDir { + confirmed, err := input.Confirm("This is not an Astro project directory. Would you like to initialize one here?") + if err != nil { + return errors.New(ansi.Red("this is not an Astro project directory.\nChange to another directory or run astro dev init to turn your working directory into an Astro project\n")) + } + if !confirmed { + return errors.New(ansi.Red("this is not an Astro project directory.\nChange to another directory or run astro dev init to turn your working directory into an Astro project\n")) + } + return airflowInit(newAirflowInitCmd(), []string{}) + } + + return nil +} diff --git a/cmd/airflow_hooks_test.go b/cmd/airflow_hooks_test.go new file mode 100644 index 000000000..0de3fb237 --- /dev/null +++ b/cmd/airflow_hooks_test.go @@ -0,0 +1,103 @@ +package cmd + +import ( + "os" + "testing" + + "github.com/astronomer/astro-cli/config" + testUtil "github.com/astronomer/astro-cli/pkg/testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/suite" +) + +type AirflowHooksSuite struct { + suite.Suite + tempDir string +} + +func TestAirflowHooks(t *testing.T) { + suite.Run(t, new(AirflowHooksSuite)) +} + +func (s *AirflowHooksSuite) SetupTest() { + testUtil.InitTestConfig(testUtil.LocalPlatform) + dir, err := os.MkdirTemp("", "test_hooks_temp_dir_*") + if err != nil { + s.T().Fatalf("failed to create temp dir: %v", err) + } + s.tempDir = dir + config.WorkingPath = s.tempDir +} + +func (s *AirflowHooksSuite) SetupSubTest() { + testUtil.InitTestConfig(testUtil.LocalPlatform) + dir, err := os.MkdirTemp("", "test_hooks_temp_dir_*") + if err != nil { + s.T().Fatalf("failed to create temp dir: %v", err) + } + s.tempDir = dir + config.WorkingPath = s.tempDir +} + +func (s *AirflowHooksSuite) TearDownTest() { + os.RemoveAll(s.tempDir) +} + +func (s *AirflowHooksSuite) TearDownSubTest() { + os.RemoveAll(s.tempDir) +} + +var ( + _ suite.SetupSubTest = (*AirflowHooksSuite)(nil) + _ suite.TearDownSubTest = (*AirflowHooksSuite)(nil) +) + +func (s *AirflowHooksSuite) TestEnsureProjectDirOrInit_AlreadyProjectDir() { + s.Run("returns nil when already an Astro project directory", func() { + // Create .astro/config.yaml to mark this as a valid Astro project + astroDir := s.tempDir + "/.astro" + s.NoError(os.MkdirAll(astroDir, 0o755)) + configFile, err := os.Create(astroDir + "/config.yaml") + s.NoError(err) + configFile.Close() + + err = EnsureProjectDirOrInit(&cobra.Command{}, []string{}) + s.NoError(err) + }) +} + +func (s *AirflowHooksSuite) TestEnsureProjectDirOrInit_NotProjectDir_UserConfirms() { + s.Run("initializes project when user confirms", func() { + // tempDir is not an Astro project dir; user inputs "y" to confirm init + defer testUtil.MockUserInput(s.T(), "y\n")() + + err := EnsureProjectDirOrInit(&cobra.Command{}, []string{}) + s.NoError(err) + + // Verify the project was initialized (Dockerfile should exist) + _, statErr := os.Stat(s.tempDir + "/Dockerfile") + s.NoError(statErr, "Dockerfile should have been created by init") + }) +} + +func (s *AirflowHooksSuite) TestEnsureProjectDirOrInit_NotProjectDir_UserDeclines() { + s.Run("returns error when user declines initialization", func() { + // tempDir is not an Astro project dir; user inputs "n" to decline + defer testUtil.MockUserInput(s.T(), "n\n")() + + err := EnsureProjectDirOrInit(&cobra.Command{}, []string{}) + s.Error(err) + s.Contains(err.Error(), "this is not an Astro project directory") + }) +} + +func (s *AirflowHooksSuite) TestEnsureProjectDirOrInit_InvalidPath() { + s.Run("returns error when path is not resolvable", func() { + config.WorkingPath = "./\000x" + + err := EnsureProjectDirOrInit(&cobra.Command{}, []string{}) + s.Error(err) + s.Contains(err.Error(), "failed to verify that your working directory is an Astro project") + }) +}