From d1896ccdbe888b9a8fa46d7fb6f6caaf80c80495 Mon Sep 17 00:00:00 2001 From: osama-asif Date: Thu, 17 Sep 2026 03:33:05 +0500 Subject: [PATCH] feat: install apps from git repositories --- README.md | 2 +- docs/app-manifest.md | 4 +- docs/cli.md | 6 +- internal/cli/app_install.go | 262 +++++++++++++++++++++++++------ internal/cli/app_install_test.go | 135 ++++++++++++++-- internal/cli/migration.go | 4 +- 6 files changed, 344 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 9a84c15..83bbd5e 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ dygo generate entity crm/contact dygo generate job crm/send-welcome-email dygo app validate -dygo app install crm +dygo app install https://github.com/acme/dygo-crm.git dygo entity validate dygo doctor diff --git a/docs/app-manifest.md b/docs/app-manifest.md index 878a63b..3e560dd 100644 --- a/docs/app-manifest.md +++ b/docs/app-manifest.md @@ -50,7 +50,7 @@ List discovered apps from the current project: ```sh dygo app list dygo app validate -dygo app install dygo-crm +dygo app install https://github.com/acme/dygo-crm.git dygo entity list dygo entity validate ``` @@ -67,4 +67,4 @@ The app manifest is currently a framework-owned metadata input. The manifest does not include an app `type` field. Core and Studio bootstrap rules are handled by framework code. -`dygo app install ` does not fetch remote source. It validates source already present under `apps/` or `.dygo/apps/`, resolves its dependency closure, and updates generated Hook and Job runner wiring. It never connects to a database. Run `dygo db migrate` explicitly to apply App metadata, patches, access, Fixtures, and lifecycle state. +`dygo app install ` clones the repository's default revision, requires `app.yml` at its root, validates the App with the current project, and copies the source to `apps/` without Git metadata. Existing dependencies must already be installed. It then updates generated Hook and Job runner wiring. It never connects to a database. Run `dygo db migrate` explicitly to apply App metadata, patches, access, Fixtures, and lifecycle state. diff --git a/docs/cli.md b/docs/cli.md index 33a44b5..0d5cc27 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -47,9 +47,9 @@ This document describes the dygo CLI command surface. Commands that are intentio - `dygo app` - Groups dygo app commands. - `dygo app list` - Lists discovered apps, versions, labels, and install locations. - `dygo app validate` - Validates app manifests, app paths, dependencies, and reserved app metadata. -- `dygo app install ` - Validates a local App and its dependencies, then updates generated Hook and Job runner wiring. App source must already exist under `apps/` or `.dygo/apps/`; run `dygo db migrate` separately to change a database. -- `dygo app install --dry-run` - Prints the local runner change without writing. -- `dygo app install --yes` - Applies the local runner change without prompting. +- `dygo app install ` - Clones an App repository, validates it with the current project, copies it to `apps/` without Git metadata, and updates generated Hook and Job runner wiring. The repository root must contain `app.yml`. Run `dygo db migrate` separately to change a database. +- `dygo app install --dry-run` - Clones and validates the App without writing project files. +- `dygo app install --yes` - Installs the App and updates runner wiring without prompting. ## Entities diff --git a/internal/cli/app_install.go b/internal/cli/app_install.go index fb0c4de..7efe98f 100644 --- a/internal/cli/app_install.go +++ b/internal/cli/app_install.go @@ -2,27 +2,36 @@ package cli import ( "bytes" + "context" "fmt" "io" + "io/fs" "os" + "os/exec" "path/filepath" "strings" "github.com/hapyco/dygo/internal/app/manifest" "github.com/hapyco/dygo/internal/app/registry" "github.com/hapyco/dygo/internal/project" + "github.com/hapyco/dygo/internal/reserved" "github.com/hapyco/dygo/internal/runnergen" "github.com/spf13/cobra" ) +type appRepositoryCheckout struct { + Root string + App manifest.LoadedApp + Commit string +} + func newAppInstallCommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Command { var dryRun, yes bool cmd := &cobra.Command{ - Use: "install ", - Short: "Validate a local App and prepare its project runner", - Long: "Validate locally available Apps and dependencies, then update the generated project runner. " + - "App source must already exist under apps/ or .dygo/apps/. " + - "Build the project runner and run dygo db migrate to apply database changes.", + Use: "install ", + Short: "Install an App from a Git repository", + Long: "Clone and validate an App repository, install its source under apps/, and update generated Hook and Job runner wiring. " + + "Run dygo db migrate separately to apply database changes.", Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { root, err := workingRootPath() @@ -32,75 +41,238 @@ func newAppInstallCommand(stdin io.Reader, stdout, stderr io.Writer) *cobra.Comm if err := runnergen.RequireGeneratedProjectRoot(root); err != nil { return fmt.Errorf("app install requires a generated dygo project: %w", err) } - // Share the existing app validate command's validation, without invoking a subprocess. - apps, err := project.LoadApps(root) + + checkout, err := cloneAppRepository(cmd.Context(), args[0]) if err != nil { return err } - order, err := appInstallOrder(apps, args[0]) + defer os.RemoveAll(checkout.Root) + + existingApps, err := project.LoadApps(root) if err != nil { return err } - // Wiring always covers the whole project, independently of database state. - update, err := runnergen.Render(root, runnergen.RenderOptions{}) - if err != nil { - return fmt.Errorf("plan app installation: %w", err) + for _, app := range existingApps { + if app.Manifest.Name == checkout.App.Manifest.Name { + return fmt.Errorf("app %q is already present at %s", app.Manifest.Name, relToHooksRoot(root, app.Dir)) + } } - before, err := os.ReadFile(update.RunnerFile) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("read project runner: %w", err) + if reserved.IsApp(checkout.App.Manifest.Name) { + return fmt.Errorf("validate cloned App: app name %q is reserved for framework-managed apps", checkout.App.Manifest.Name) + } + + target := filepath.Join(root, "apps", checkout.App.Manifest.Name) + plannedApp := checkout.App + plannedApp.Dir = target + plannedApp.ManifestPath = filepath.Join(target, manifest.Filename) + apps := append(append([]manifest.LoadedApp(nil), existingApps...), plannedApp) + // Share the manifest-set validation used by `dygo app validate` before + // adding source to the project. + if err := manifest.ValidateSet(apps); err != nil { + return fmt.Errorf("validate cloned App with project: %w", err) } - missing := os.IsNotExist(err) - status, err := runnergen.GeneratedFileStatus(update.RunnerFile, update.Source, true) + order, err := appInstallOrder(apps, checkout.App.Manifest.Name) if err != nil { return err } - if _, err := fmt.Fprintf(stdout, "app install plan\nproject: %s\n%d apps are valid\nprepare order: %s\nrunner: %s (%s; all local Apps)\n", root, len(apps), strings.Join(order, " -> "), relToHooksRoot(root, update.RunnerFile), status); err != nil { + + runnerFile := filepath.Join(root, "cmd", "dygo", "main.go") + if err := runnergen.PreflightGeneratedFile(runnerFile, runnergen.UpgradeManualSnippet()); err != nil { + return err + } + if _, err := fmt.Fprintf(stdout, "app install plan\nproject: %s\napp: %s %s\nsource commit: %s\ndestination: %s (create)\n%d apps are valid\nprepare order: %s\nrunner: %s (regenerate for all project Apps)\n", root, checkout.App.Manifest.Name, checkout.App.Manifest.Version, checkout.Commit, relToHooksRoot(root, target), len(apps), strings.Join(order, " -> "), relToHooksRoot(root, runnerFile)); err != nil { return fmt.Errorf("write app install plan: %w", err) } if dryRun { - _, err := fmt.Fprintln(stdout, "dry-run: no files or database changes") + _, err := fmt.Fprintln(stdout, "dry-run: no project files or database changes") return err } - if status != "unchanged" { - if !yes { - confirmed, err := streamConfirmer(stdin, stderr)(cmd.Context(), "Apply local App installation?") - if err != nil { - return err - } - if !confirmed { - _, err := fmt.Fprintln(stdout, "App installation cancelled.") - return err - } - } - // Reject edits made while the user reviewed the plan; write only its rendered source. - current, err := runnergen.Render(root, runnergen.RenderOptions{}) + if !yes { + confirmed, err := streamConfirmer(stdin, stderr)(cmd.Context(), "Install App source and update the project runner?") if err != nil { return err } - existing, err := os.ReadFile(update.RunnerFile) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("read project runner: %w", err) - } - if !bytes.Equal(current.Source, update.Source) || !bytes.Equal(before, existing) || missing != os.IsNotExist(err) { - return fmt.Errorf("project runner plan changed; rerun dygo app install %s", args[0]) + if !confirmed { + _, err := fmt.Fprintln(stdout, "App installation cancelled.") + return err } - if err := os.MkdirAll(filepath.Dir(update.RunnerFile), 0o755); err != nil { - return fmt.Errorf("create project runner directory: %w", err) + } + + if err := installCheckout(root, checkout); err != nil { + return err + } + keepTarget := false + defer func() { + if !keepTarget { + _ = os.RemoveAll(target) } - if _, err := runnergen.WriteFileIfChanged(update.RunnerFile, update.Source); err != nil { - return err + }() + + // Run the same project-level validation used by `dygo app validate` + // against the installed paths before updating generated code. + if _, err := project.LoadApps(root); err != nil { + return err + } + before, err := os.ReadFile(runnerFile) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("read project runner: %w", err) + } + runnerMissing := os.IsNotExist(err) + update, err := runnergen.Render(root, runnergen.RenderOptions{}) + if err != nil { + return fmt.Errorf("prepare project runner: %w", err) + } + current, err := os.ReadFile(runnerFile) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("read project runner: %w", err) + } + if !bytes.Equal(before, current) || runnerMissing != os.IsNotExist(err) { + return fmt.Errorf("project runner changed during App installation; rerun dygo app install") + } + if err := os.MkdirAll(filepath.Dir(runnerFile), 0o755); err != nil { + return fmt.Errorf("create project runner directory: %w", err) + } + if _, err := runnergen.WriteFileIfChanged(update.RunnerFile, update.Source); err != nil { + if restoreErr := restoreRunner(runnerFile, before, runnerMissing); restoreErr != nil { + return fmt.Errorf("write project runner: %v; restore runner: %w", err, restoreErr) } + return err } - _, err = fmt.Fprintf(stdout, "Apps prepared: %s\nNext: build the project runner, then run dygo db migrate.\n", strings.Join(order, ", ")) + keepTarget = true + _, err = fmt.Fprintf(stdout, "App installed: %s %s\nApps prepared: %s\nNext: build the project runner, then run dygo db migrate.\n", checkout.App.Manifest.Name, checkout.App.Manifest.Version, strings.Join(order, ", ")) return err }, } - cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show local installation changes without writing") + cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Clone and validate without writing project files") cmd.Flags().BoolVar(&yes, "yes", false, "Skip the local installation confirmation") return cmd } +func cloneAppRepository(ctx context.Context, source string) (appRepositoryCheckout, error) { + if strings.TrimSpace(source) == "" { + return appRepositoryCheckout{}, fmt.Errorf("App repository URL is required") + } + root, err := os.MkdirTemp("", "dygo-app-repository-*") + if err != nil { + return appRepositoryCheckout{}, fmt.Errorf("create App repository checkout: %w", err) + } + checkout := filepath.Join(root, "source") + if err := exec.CommandContext(ctx, "git", "clone", "--quiet", "--depth", "1", "--", source, checkout).Run(); err != nil { + _ = os.RemoveAll(root) + return appRepositoryCheckout{}, fmt.Errorf("clone App repository: %w", err) + } + app, err := manifest.LoadAppDir(checkout) + if err != nil { + _ = os.RemoveAll(root) + return appRepositoryCheckout{}, fmt.Errorf("validate cloned App: %w", err) + } + commitOutput, err := exec.CommandContext(ctx, "git", "-C", checkout, "rev-parse", "HEAD").Output() + if err != nil { + _ = os.RemoveAll(root) + return appRepositoryCheckout{}, fmt.Errorf("read cloned App revision: %w", err) + } + return appRepositoryCheckout{Root: root, App: app, Commit: strings.TrimSpace(string(commitOutput))}, nil +} + +func installCheckout(root string, checkout appRepositoryCheckout) error { + target := filepath.Join(root, "apps", checkout.App.Manifest.Name) + if _, err := os.Stat(target); err == nil { + return fmt.Errorf("App destination already exists: %s", relToHooksRoot(root, target)) + } else if !os.IsNotExist(err) { + return fmt.Errorf("check App destination: %w", err) + } + stagingRoot := filepath.Join(root, ".dygo", "tmp") + if err := os.MkdirAll(stagingRoot, 0o755); err != nil { + return fmt.Errorf("create App installation staging directory: %w", err) + } + staging, err := os.MkdirTemp(stagingRoot, "app-install-*") + if err != nil { + return fmt.Errorf("create App installation staging directory: %w", err) + } + defer os.RemoveAll(staging) + if err := copyAppRepository(checkout.App.Dir, staging); err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil { + return fmt.Errorf("create project Apps directory: %w", err) + } + if err := os.Rename(staging, target); err != nil { + return fmt.Errorf("install App source: %w", err) + } + return nil +} + +func copyAppRepository(source, target string) error { + return filepath.WalkDir(source, func(path string, entry fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + relative, err := filepath.Rel(source, path) + if err != nil { + return err + } + if relative == "." { + return nil + } + if entry.Name() == ".git" && entry.IsDir() { + return filepath.SkipDir + } + if entry.Type()&os.ModeSymlink != 0 { + return fmt.Errorf("install App source: symbolic links are not supported: %s", filepath.ToSlash(relative)) + } + destination := filepath.Join(target, relative) + info, err := entry.Info() + if err != nil { + return err + } + if entry.IsDir() { + if err := os.MkdirAll(destination, info.Mode().Perm()); err != nil { + return fmt.Errorf("create App directory %s: %w", filepath.ToSlash(relative), err) + } + return nil + } + if !info.Mode().IsRegular() { + return fmt.Errorf("install App source: unsupported file type: %s", filepath.ToSlash(relative)) + } + if err := copyAppFile(path, destination, info.Mode().Perm()); err != nil { + return fmt.Errorf("copy App file %s: %w", filepath.ToSlash(relative), err) + } + return nil + }) +} + +func copyAppFile(source, destination string, mode fs.FileMode) (copyErr error) { + input, err := os.Open(source) + if err != nil { + return err + } + defer input.Close() + output, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode) + if err != nil { + return err + } + defer func() { + if err := output.Close(); copyErr == nil { + copyErr = err + } + if copyErr != nil { + _ = os.Remove(destination) + } + }() + _, copyErr = io.Copy(output, input) + return copyErr +} + +func restoreRunner(path string, before []byte, missing bool) error { + if missing { + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + return err + } + return nil + } + return os.WriteFile(path, before, 0o644) +} + func appInstallOrder(apps []manifest.LoadedApp, target string) ([]string, error) { ordered, err := registry.DependencyOrder(apps) if err != nil { @@ -120,7 +292,7 @@ func appInstallOrder(apps []manifest.LoadedApp, target string) ([]string, error) } } if !found { - return nil, fmt.Errorf("app %q was not found; add its source under apps/ or .dygo/apps/ before installation", target) + return nil, fmt.Errorf("app %q was not found in the validated project", target) } var names []string for _, app := range ordered { diff --git a/internal/cli/app_install_test.go b/internal/cli/app_install_test.go index db4528f..5a63a54 100644 --- a/internal/cli/app_install_test.go +++ b/internal/cli/app_install_test.go @@ -4,12 +4,13 @@ import ( "bytes" "context" "os" + "os/exec" "path/filepath" "strings" "testing" ) -func TestAppInstallPreparesDependencyClosureAndRunner(t *testing.T) { +func TestAppInstallClonesValidatesAndInstallsRepository(t *testing.T) { root := t.TempDir() writeCLIProjectRoot(t, root) writeCLIGoModule(t, root, "example.com/acme") @@ -20,55 +21,157 @@ label: CRM version: 0.1.0 dependencies: [core] `) - writeCLIAppWithBody(t, filepath.Join(root, "apps", "sales"), ` + repository := writeGitAppRepository(t, ` name: sales label: Sales version: 0.1.0 dependencies: [crm] `) + if err := os.WriteFile(filepath.Join(repository, "README.md"), []byte("sales\n"), 0o644); err != nil { + t.Fatal(err) + } + commitGitAppRepository(t, repository) t.Chdir(root) var stdout bytes.Buffer - if err := Run(context.Background(), []string{"app", "install", "sales", "--yes"}, strings.NewReader(""), &stdout, &bytes.Buffer{}); err != nil { + if err := Run(context.Background(), []string{"app", "install", repository, "--yes"}, strings.NewReader(""), &stdout, &bytes.Buffer{}); err != nil { t.Fatal(err) } - for _, want := range []string{"prepare order: core -> crm -> sales", "Next: build the project runner, then run dygo db migrate."} { + for _, want := range []string{ + "app: sales 0.1.0", + "destination: apps/sales (create)", + "prepare order: core -> crm -> sales", + "App installed: sales 0.1.0", + "Next: build the project runner, then run dygo db migrate.", + } { if !strings.Contains(stdout.String(), want) { t.Fatalf("stdout = %q, want %q", stdout.String(), want) } } - if _, err := os.Stat(filepath.Join(root, "cmd", "dygo", "main.go")); err != nil { - t.Fatalf("runner was not written: %v", err) + for _, path := range []string{ + filepath.Join(root, "apps", "sales", "app.yml"), + filepath.Join(root, "apps", "sales", "README.md"), + filepath.Join(root, "cmd", "dygo", "main.go"), + } { + if _, err := os.Stat(path); err != nil { + t.Fatalf("installed file %s: %v", path, err) + } + } + if _, err := os.Stat(filepath.Join(root, "apps", "sales", ".git")); !os.IsNotExist(err) { + t.Fatalf("installed Git metadata stat = %v, want missing", err) } } -func TestAppInstallDryRunDoesNotWrite(t *testing.T) { +func TestAppInstallDryRunLeavesProjectUnchanged(t *testing.T) { root := t.TempDir() writeCLIProjectRoot(t, root) writeCLIGoModule(t, root, "example.com/acme") - writeCLIApp(t, filepath.Join(root, "apps", "sales"), "sales") + repository := writeGitAppRepository(t, "name: sales\nlabel: Sales\nversion: 0.1.0\n") + commitGitAppRepository(t, repository) t.Chdir(root) var stdout bytes.Buffer - if err := Run(context.Background(), []string{"app", "install", "sales", "--dry-run"}, strings.NewReader(""), &stdout, &bytes.Buffer{}); err != nil { + if err := Run(context.Background(), []string{"app", "install", repository, "--dry-run"}, strings.NewReader(""), &stdout, &bytes.Buffer{}); err != nil { t.Fatal(err) } - if !strings.Contains(stdout.String(), "dry-run: no files or database changes") { + if !strings.Contains(stdout.String(), "dry-run: no project files or database changes") { t.Fatalf("stdout = %q", stdout.String()) } - if _, err := os.Stat(filepath.Join(root, "cmd", "dygo", "main.go")); !os.IsNotExist(err) { - t.Fatalf("dry-run runner stat = %v, want missing", err) + for _, path := range []string{filepath.Join(root, "apps", "sales"), filepath.Join(root, "cmd", "dygo", "main.go")} { + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("dry-run path %s stat = %v, want missing", path, err) + } + } +} + +func TestAppInstallRejectsInvalidRepository(t *testing.T) { + root := t.TempDir() + writeCLIProjectRoot(t, root) + writeCLIGoModule(t, root, "example.com/acme") + repository := writeGitAppRepository(t, "") + if err := os.WriteFile(filepath.Join(repository, "README.md"), []byte("not an App\n"), 0o644); err != nil { + t.Fatal(err) + } + commitGitAppRepository(t, repository) + t.Chdir(root) + + err := Run(context.Background(), []string{"app", "install", repository, "--yes"}, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}) + if err == nil || !strings.Contains(err.Error(), "validate cloned App") { + t.Fatalf("error = %v, want cloned App validation error", err) + } +} + +func TestAppInstallRejectsMissingDependencyWithoutWriting(t *testing.T) { + root := t.TempDir() + writeCLIProjectRoot(t, root) + writeCLIGoModule(t, root, "example.com/acme") + repository := writeGitAppRepository(t, ` +name: sales +label: Sales +version: 0.1.0 +dependencies: [crm] +`) + commitGitAppRepository(t, repository) + t.Chdir(root) + + err := Run(context.Background(), []string{"app", "install", repository, "--yes"}, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}) + if err == nil || !strings.Contains(err.Error(), `depends on unknown app "crm"`) { + t.Fatalf("error = %v, want missing dependency", err) + } + if _, statErr := os.Stat(filepath.Join(root, "apps", "sales")); !os.IsNotExist(statErr) { + t.Fatalf("App destination stat = %v, want missing", statErr) } } -func TestAppInstallRejectsUnknownApp(t *testing.T) { +func TestAppInstallRemovesSourceWhenProjectValidationFails(t *testing.T) { root := t.TempDir() writeCLIProjectRoot(t, root) writeCLIGoModule(t, root, "example.com/acme") + repository := writeGitAppRepository(t, "name: sales\nlabel: Sales\nversion: 0.1.0\n") + entityDir := filepath.Join(repository, "entities", "lead") + if err := os.MkdirAll(entityDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(entityDir, "lead.entity.yml"), []byte("name: [\n"), 0o644); err != nil { + t.Fatal(err) + } + commitGitAppRepository(t, repository) t.Chdir(root) - err := Run(context.Background(), []string{"app", "install", "missing", "--yes"}, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}) - if err == nil || !strings.Contains(err.Error(), `app "missing" was not found`) { - t.Fatalf("error = %v", err) + err := Run(context.Background(), []string{"app", "install", repository, "--yes"}, strings.NewReader(""), &bytes.Buffer{}, &bytes.Buffer{}) + if err == nil || !strings.Contains(err.Error(), "prepare project runner") { + t.Fatalf("error = %v, want project validation error", err) + } + if _, statErr := os.Stat(filepath.Join(root, "apps", "sales")); !os.IsNotExist(statErr) { + t.Fatalf("App destination stat = %v, want rollback", statErr) + } + if _, statErr := os.Stat(filepath.Join(root, "cmd", "dygo", "main.go")); !os.IsNotExist(statErr) { + t.Fatalf("runner stat = %v, want missing", statErr) + } +} + +func writeGitAppRepository(t *testing.T, manifest string) string { + t.Helper() + repository := t.TempDir() + if strings.TrimSpace(manifest) != "" { + if err := os.WriteFile(filepath.Join(repository, "app.yml"), []byte(strings.TrimSpace(manifest)+"\n"), 0o644); err != nil { + t.Fatal(err) + } + } + runGit(t, repository, "init", "--quiet") + return repository +} + +func commitGitAppRepository(t *testing.T, repository string) { + t.Helper() + runGit(t, repository, "add", ".") + runGit(t, repository, "-c", "user.name=dygo", "-c", "user.email=dygo@example.invalid", "commit", "--quiet", "-m", "initial") +} + +func runGit(t *testing.T, repository string, args ...string) { + t.Helper() + commandArgs := append([]string{"-C", repository}, args...) + if output, err := exec.Command("git", commandArgs...).CombinedOutput(); err != nil { + t.Fatalf("git %s: %v: %s", strings.Join(args, " "), err, output) } } diff --git a/internal/cli/migration.go b/internal/cli/migration.go index 0ea6ec1..d507bb4 100644 --- a/internal/cli/migration.go +++ b/internal/cli/migration.go @@ -31,7 +31,7 @@ func migrationCodeValidator(hooks *db.RecordHookRegistry, jobs *jobruntime.Regis path := filepath.Join(filepath.Dir(entity.Path), "hooks.go") if _, err := os.Stat(path); err == nil { if !hooks.HasEntity(entity.AppName, entity.Entity.Name) { - return fmt.Errorf("App %s Entity %s Hooks are not compiled; run dygo app install %s and build the project runner", entity.AppName, entity.Entity.Name, entity.AppName) + return fmt.Errorf("App %s Entity %s Hooks are not compiled; run dygo hook sync and build the project runner", entity.AppName, entity.Entity.Name) } } else if !os.IsNotExist(err) { return err @@ -43,7 +43,7 @@ func migrationCodeValidator(hooks *db.RecordHookRegistry, jobs *jobruntime.Regis continue } if !jobs.HasJob(job.AppName, job.Job.Name) { - return fmt.Errorf("App %s Job %s is not compiled; run dygo app install %s and build the project runner", job.AppName, job.Job.Name, job.AppName) + return fmt.Errorf("App %s Job %s is not compiled; run dygo hook sync and build the project runner", job.AppName, job.Job.Name) } } return nil