diff --git a/Makefile b/Makefile index 18a75ef..8fb79cc 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,17 @@ -.PHONY: build deps lint test +.PHONY: build deps lint test work install BINARY_NAME=sitectl-drupal +INSTALL_DIR ?= $(or $(dir $(shell which $(BINARY_NAME) 2>/dev/null)),/usr/local/bin/) -deps: - go get . +deps: work go mod tidy build: deps go build -o $(BINARY_NAME) . +install: work build + sudo cp $(BINARY_NAME) $(INSTALL_DIR)$(BINARY_NAME) + lint: go fmt ./... golangci-lint run @@ -22,3 +25,6 @@ lint: test: build go test -v -race ./... + +work: + ./scripts/use-go-work.sh diff --git a/cmd/backup.go b/cmd/backup.go index 35c9b92..32f107e 100644 --- a/cmd/backup.go +++ b/cmd/backup.go @@ -1,7 +1,6 @@ package cmd import ( - "context" "fmt" "time" @@ -58,7 +57,7 @@ Example: cmdArgs = append(cmdArgs, extraFlag) } - exitCode, err := sdk.ExecInContainerInteractive(context.Background(), containerName, cmdArgs) + exitCode, err := cli.ExecInteractive(cmd.Context(), containerName, cmdArgs) if err != nil { return err } diff --git a/cmd/drush.go b/cmd/drush.go index 774d9fd..4bd9c6f 100644 --- a/cmd/drush.go +++ b/cmd/drush.go @@ -1,7 +1,6 @@ package cmd import ( - "context" "fmt" "github.com/kballard/go-shellquote" @@ -41,7 +40,7 @@ Examples: drushCmd := []string{"bash", "-c", fmt.Sprintf("drush %s", shellquote.Join(filteredArgs...))} // Execute the command interactively using SDK helper - exitCode, err := sdk.ExecInContainerInteractive(context.Background(), containerName, drushCmd) + exitCode, err := cli.ExecInteractive(cmd.Context(), containerName, drushCmd) if err != nil { return err } diff --git a/cmd/extensions.go b/cmd/extensions.go new file mode 100644 index 0000000..9afbecb --- /dev/null +++ b/cmd/extensions.go @@ -0,0 +1,447 @@ +package cmd + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + + "charm.land/lipgloss/v2" + "github.com/kballard/go-shellquote" + "github.com/libops/sitectl/pkg/config" + "github.com/libops/sitectl/pkg/docker" + "github.com/libops/sitectl/pkg/plugin" + "github.com/spf13/cobra" + "golang.org/x/term" + "gopkg.in/yaml.v3" +) + +var drupalComponentName string +var drupalRootfsPath string + +const ( + cachePageWarningThreshold = int64(1 << 30) + pageCacheExclusionURL = "https://www.drupal.org/project/page_cache_exclusion" +) + +var ( + debugPanelStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("#112235")). + Padding(1, 2) + debugTitleStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#98C1D9")) + debugSectionDividerStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#29425E")) + debugStatusOKStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#7BD389")) + debugStatusWarningStyle = lipgloss.NewStyle(). + Bold(true). + Foreground(lipgloss.Color("#F4C95D")) + debugMutedStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#9FB3C8")) + debugRowStyle = lipgloss.NewStyle(). + Background(lipgloss.Color("#112235")) +) + +var componentExtensionCmd = &cobra.Command{ + Use: "__component", + Short: "Internal component extension command", + Hidden: true, +} + +var componentExtensionDescribeCmd = &cobra.Command{ + Use: "describe", + Short: "Internal component describe hook", + RunE: func(cmd *cobra.Command, args []string) error { + if strings.TrimSpace(drupalComponentName) != "" { + return fmt.Errorf("unknown drupal component %q", drupalComponentName) + } + _, err := fmt.Fprintln(cmd.OutOrStdout(), "== drupal components ==\nNo Drupal-specific components are registered yet.") + return err + }, +} + +var componentExtensionReconcileCmd = &cobra.Command{ + Use: "reconcile", + Short: "Internal component reconcile hook", + RunE: func(cmd *cobra.Command, args []string) error { + if strings.TrimSpace(drupalComponentName) != "" { + return fmt.Errorf("unknown drupal component %q", drupalComponentName) + } + return nil + }, +} + +var componentExtensionSetCmd = &cobra.Command{ + Use: "set [disposition]", + Short: "Internal component set hook", + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + return fmt.Errorf("unknown drupal component %q", args[0]) + }, +} + +var debugExtensionCmd = &cobra.Command{ + Use: "__debug", + Short: "Internal debug extension command", + Hidden: true, + RunE: func(cmd *cobra.Command, args []string) error { + rendered, err := renderDrupalDebug(cmd.Context()) + if err != nil { + return err + } + _, err = fmt.Fprintln(cmd.OutOrStdout(), rendered) + return err + }, +} + +func init() { + componentExtensionDescribeCmd.Flags().StringVarP(&drupalComponentName, "component", "c", "", "Specific Drupal component to describe") + componentExtensionReconcileCmd.Flags().StringVarP(&drupalComponentName, "component", "c", "", "Specific Drupal component to reconcile") + + componentExtensionCmd.AddCommand(componentExtensionDescribeCmd) + componentExtensionCmd.AddCommand(componentExtensionReconcileCmd) + componentExtensionCmd.AddCommand(componentExtensionSetCmd) + + debugExtensionCmd.Flags().StringVar(&drupalRootfsPath, "drupal-rootfs", "drupal/rootfs/var/www/drupal", "Drupal rootfs path override") +} + +func renderDrupalDebug(runCtx context.Context) (string, error) { + slog.Debug("starting plugin debug", "plugin", "drupal") + if sdk == nil { + return "", fmt.Errorf("plugin sdk is not initialized") + } + ctx, err := sdk.GetContext() + if err != nil { + return "", err + } + slog.Debug("resolved plugin context", "plugin", "drupal", "context", ctx.Name, "project_dir", ctx.ProjectDir) + slog.Debug("creating file accessor", "plugin", "drupal") + files, err := sdk.GetFileAccessor() + if err != nil { + return "", err + } + defer files.Close() + + slog.Debug("resolving drupal root", "plugin", "drupal", "rootfs", drupalRootfsPath) + drupalRoot := resolveDrupalRoot(files, ctx.ProjectDir, drupalRootfsPath) + slog.Debug("resolved drupal root", "plugin", "drupal", "drupal_root", drupalRoot) + configDir := filepath.Join(drupalRoot, "config", "sync") + body := []string{ + debugDivider(), + "", + debugTitleStyle.Render("General"), + "", + formatDebugRows([]debugRow{ + {Label: "Context", Value: ctx.Name}, + {Label: "Project dir", Value: ctx.ProjectDir}, + {Label: "Drupal root", Value: drupalRoot}, + {Label: "Config sync dir", Value: configDir}, + }), + } + + if strings.TrimSpace(drupalRoot) == "" { + slog.Debug("drupal root unavailable; skipping extension scan", "plugin", "drupal") + body = append(body, "", "Installed modules: unavailable") + return renderDebugPanel("drupal", strings.Join(body, "\n")), nil + } + + slog.Debug("reading core.extension.yml", "plugin", "drupal", "path", filepath.Join(configDir, "core.extension.yml")) + modules, themes, err := readCoreExtension(runCtx, files, filepath.Join(configDir, "core.extension.yml")) + if err != nil { + return "", err + } + slog.Debug("read installed extensions", "plugin", "drupal", "modules", len(modules), "themes", len(themes)) + slog.Debug("rendering cache_page summary", "plugin", "drupal") + cachePageSummary, err := renderCachePageSummary(runCtx) + if err != nil { + body = append(body, "", debugDivider(), "", debugTitleStyle.Render("Cache Page"), "", formatDebugRows([]debugRow{ + {Label: "Status", Value: renderStatus("warning")}, + {Label: "cache_page", Value: fmt.Sprintf("unavailable (%v)", err)}, + })) + } else if strings.TrimSpace(cachePageSummary) != "" { + body = append(body, "", debugDivider(), "", debugTitleStyle.Render("Cache Page"), "", cachePageSummary) + } + + configLines := []string{debugDivider(), "", debugTitleStyle.Render("Installed Extensions"), "", fmt.Sprintf("Installed modules (%d):", len(modules))} + configLines = append(configLines, formatListLines(modules, 3)...) + configLines = append(configLines, "") + configLines = append(configLines, fmt.Sprintf("Installed themes (%d):", len(themes))) + configLines = append(configLines, formatListLines(themes, 3)...) + body = append(body, "", strings.Join(configLines, "\n")) + + slog.Debug("finished plugin debug", "plugin", "drupal") + return renderDebugPanel("drupal", strings.Join(body, "\n")), nil +} + +func renderCachePageSummary(runCtx context.Context) (string, error) { + _, cli, containerName, err := getDrupalContainerForSDK(runCtx) + if err != nil { + return "", err + } + defer cli.Close() + + query := "SELECT COALESCE(data_length + index_length, 0) FROM information_schema.TABLES WHERE table_schema = DATABASE() AND table_name = 'cache_page';" + output, err := execDrupalCommandCapture(runCtx, cli, containerName, []string{"drush", "sql:query", query, "--extra=--batch", "--extra=--skip-column-names"}) + if err != nil { + return "", err + } + + size, err := parseFirstInt(output) + if err != nil { + return "", err + } + + rows := []debugRow{ + {Label: "Status", Value: renderStatus("ok")}, + {Label: "cache_page", Value: humanBytes(size)}, + } + if size >= cachePageWarningThreshold { + rows[0].Value = renderStatus("warning") + rows = append(rows, debugRow{Label: "Recommendation", Value: pageCacheExclusionURL}) + } + return formatDebugRows(rows), nil +} + +func getDrupalContainerForSDK(runCtx context.Context) (ctx *config.Context, cli *docker.DockerClient, containerName string, err error) { + if sdk == nil { + return nil, nil, "", fmt.Errorf("plugin sdk is not initialized") + } + + ctx, err = sdk.GetContext() + if err != nil { + return nil, nil, "", err + } + + cli, err = sdk.GetDockerClient() + if err != nil { + return nil, nil, "", err + } + + containerName, err = cli.GetContainerNameContext(runCtx, ctx, *drupalServiceName) + if err != nil { + cli.Close() + return nil, nil, "", err + } + + return ctx, cli, containerName, nil +} + +func execDrupalCommandCapture(runCtx context.Context, cli *docker.DockerClient, containerName string, cmd []string) (string, error) { + slog.Debug(strings.Join(cmd, " "), "plugin", "drupal", "container", containerName) + var stdout bytes.Buffer + var stderr bytes.Buffer + + wrappedCmd := []string{"bash", "-lc", fmt.Sprintf("cd /var/www/drupal && %s", shellquote.Join(cmd...))} + + exitCode, err := cli.Exec(runCtx, docker.ExecOptions{ + Container: containerName, + Cmd: wrappedCmd, + WorkingDir: "/var/www/drupal", + AttachStdout: true, + AttachStderr: true, + Stdout: &stdout, + Stderr: &stderr, + }) + if err != nil { + return "", err + } + if exitCode != 0 { + detail := strings.TrimSpace(stderr.String()) + if detail == "" { + detail = strings.TrimSpace(stdout.String()) + } + if detail != "" { + return "", fmt.Errorf("drupal command failed with exit code %d: %s", exitCode, detail) + } + return "", fmt.Errorf("drupal command failed with exit code %d", exitCode) + } + + return strings.TrimSpace(stdout.String()), nil +} + +func parseFirstInt(output string) (int64, error) { + for _, line := range strings.Split(output, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + return strconv.ParseInt(line, 10, 64) + } + return 0, fmt.Errorf("no numeric output returned") +} + +func humanBytes(size int64) string { + const unit = 1024 + if size < unit { + return fmt.Sprintf("%dB", size) + } + div, exp := int64(unit), 0 + for n := size / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f%ciB", float64(size)/float64(div), "KMGTPE"[exp]) +} + +func resolveDrupalRoot(files *plugin.FileAccessor, projectDir, drupalRootPath string) string { + candidates := []string{} + if trimmed := strings.TrimSpace(drupalRootPath); trimmed != "" { + if filepath.IsAbs(trimmed) { + candidates = append(candidates, filepath.Clean(trimmed)) + } else { + candidates = append(candidates, filepath.Join(projectDir, trimmed)) + } + } + if strings.TrimSpace(projectDir) != "" { + candidates = append(candidates, projectDir) + } + for _, candidate := range candidates { + if _, err := files.ReadFile(filepath.Join(candidate, "config", "sync", "core.extension.yml")); err == nil { + return candidate + } + } + return "" +} + +func readCoreExtension(runCtx context.Context, files *plugin.FileAccessor, path string) ([]string, []string, error) { + data, err := files.ReadFileContext(runCtx, path) + if err != nil { + if os.IsNotExist(err) { + return nil, nil, nil + } + return nil, nil, err + } + + var extension struct { + Module map[string]int `yaml:"module"` + Theme map[string]int `yaml:"theme"` + } + if err := yaml.Unmarshal(data, &extension); err != nil { + return nil, nil, err + } + + modules := make([]string, 0, len(extension.Module)) + for name := range extension.Module { + modules = append(modules, name) + } + sort.Strings(modules) + + themes := make([]string, 0, len(extension.Theme)) + for name := range extension.Theme { + themes = append(themes, name) + } + sort.Strings(themes) + + return modules, themes, nil +} + +func formatListLines(values []string, perLine int) []string { + if len(values) == 0 { + return []string{" none"} + } + if perLine <= 0 { + perLine = 10 + } + + lines := make([]string, 0, (len(values)+perLine-1)/perLine) + for i := 0; i < len(values); i += perLine { + end := i + perLine + if end > len(values) { + end = len(values) + } + lines = append(lines, " "+strings.Join(values[i:end], ", ")) + } + return lines +} + +type debugRow struct { + Label string + Value string +} + +func renderDebugPanel(title, body string) string { + header := debugTitleStyle.Render(strings.TrimSpace(title)) + content := header + if strings.TrimSpace(body) != "" { + content += "\n\n" + body + } + return debugPanelStyle.Width(debugPanelWidth()).Render(content) +} + +func formatDebugRows(rows []debugRow) string { + labelWidth := 0 + for _, row := range rows { + if width := len(strings.TrimSpace(row.Label)); width > labelWidth { + labelWidth = width + } + } + lines := make([]string, 0, len(rows)) + rowWidth := debugContentWidth() + for _, row := range rows { + label := strings.TrimSpace(row.Label) + value := strings.TrimSpace(row.Value) + if label == "" { + lines = append(lines, renderDebugRow(rowWidth, "", value)) + continue + } + lines = append(lines, renderDebugRow(rowWidth, fmt.Sprintf("%-*s", labelWidth, label), value)) + } + return strings.Join(lines, "\n") +} + +func renderStatus(state string) string { + switch strings.ToLower(strings.TrimSpace(state)) { + case "ok": + return debugStatusOKStyle.Render("OK") + case "warning": + return debugStatusWarningStyle.Render("WARNING") + default: + return debugMutedStyle.Render(strings.ToUpper(strings.TrimSpace(state))) + } +} + +func renderDebugRow(width int, label, value string) string { + valueWidth := max(0, width-lipgloss.Width(label)-2) + row := label + if strings.TrimSpace(label) != "" { + row += " " + } + row += lipgloss.NewStyle(). + Width(valueWidth). + Background(lipgloss.Color("#112235")). + Render(value) + return debugRowStyle.Width(width).Render(row) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func debugPanelWidth() int { + if columns, err := strconv.Atoi(strings.TrimSpace(os.Getenv("COLUMNS"))); err == nil && columns > 0 { + return max(40, columns) + } + if width, _, err := term.GetSize(int(os.Stdout.Fd())); err == nil && width > 0 { + return max(40, width) + } + return 100 +} + +func debugContentWidth() int { + return max(20, debugPanelWidth()-4) +} + +func debugDivider() string { + return debugSectionDividerStyle.Width(debugContentWidth()).Render(strings.Repeat("─", debugContentWidth())) +} diff --git a/cmd/extensions_test.go b/cmd/extensions_test.go new file mode 100644 index 0000000..8b0920a --- /dev/null +++ b/cmd/extensions_test.go @@ -0,0 +1,186 @@ +package cmd + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/libops/sitectl/pkg/config" + "github.com/libops/sitectl/pkg/plugin" +) + +func TestReadCoreExtensionParsesModulesAndThemes(t *testing.T) { + root := t.TempDir() + path := filepath.Join(root, "core.extension.yml") + data := `_core: + default_config_hash: abc +module: + views: 10 + pathauto: 1 + system: 0 +theme: + claro: 0 + olivero: 0 +` + if err := os.WriteFile(path, []byte(data), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + files, err := plugin.NewFileAccessor(&config.Context{DockerHostType: config.ContextLocal}) + if err != nil { + t.Fatalf("NewFileAccessor() error = %v", err) + } + defer files.Close() + + modules, themes, err := readCoreExtension(context.Background(), files, path) + if err != nil { + t.Fatalf("readCoreExtension() error = %v", err) + } + + wantModules := []string{"pathauto", "system", "views"} + if !reflect.DeepEqual(modules, wantModules) { + t.Fatalf("modules = %v, want %v", modules, wantModules) + } + + wantThemes := []string{"claro", "olivero"} + if !reflect.DeepEqual(themes, wantThemes) { + t.Fatalf("themes = %v, want %v", themes, wantThemes) + } +} + +func TestReadCoreExtensionMissingFileReturnsNilSlices(t *testing.T) { + files, err := plugin.NewFileAccessor(&config.Context{DockerHostType: config.ContextLocal}) + if err != nil { + t.Fatalf("NewFileAccessor() error = %v", err) + } + defer files.Close() + + modules, themes, err := readCoreExtension(context.Background(), files, filepath.Join(t.TempDir(), "missing.yml")) + if err != nil { + t.Fatalf("readCoreExtension() error = %v", err) + } + if modules != nil { + t.Fatalf("expected nil modules, got %v", modules) + } + if themes != nil { + t.Fatalf("expected nil themes, got %v", themes) + } +} + +func TestResolveDrupalRootFindsConfiguredRootfs(t *testing.T) { + projectDir := t.TempDir() + drupalRoot := filepath.Join(projectDir, "drupal", "rootfs", "var", "www", "drupal") + configDir := filepath.Join(drupalRoot, "config", "sync") + if err := os.MkdirAll(configDir, 0o755); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + if err := os.WriteFile(filepath.Join(configDir, "core.extension.yml"), []byte("module: {}\ntheme: {}\n"), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + files, err := plugin.NewFileAccessor(&config.Context{DockerHostType: config.ContextLocal}) + if err != nil { + t.Fatalf("NewFileAccessor() error = %v", err) + } + defer files.Close() + + got := resolveDrupalRoot(files, projectDir, "drupal/rootfs/var/www/drupal") + if got != drupalRoot { + t.Fatalf("resolveDrupalRoot() = %q, want %q", got, drupalRoot) + } +} + +func TestResolveDrupalRootFallsBackToProjectDir(t *testing.T) { + projectDir := t.TempDir() + configDir := filepath.Join(projectDir, "config", "sync") + if err := os.MkdirAll(configDir, 0o755); err != nil { + t.Fatalf("MkdirAll() error = %v", err) + } + if err := os.WriteFile(filepath.Join(configDir, "core.extension.yml"), []byte("module: {}\ntheme: {}\n"), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + files, err := plugin.NewFileAccessor(&config.Context{DockerHostType: config.ContextLocal}) + if err != nil { + t.Fatalf("NewFileAccessor() error = %v", err) + } + defer files.Close() + + got := resolveDrupalRoot(files, projectDir, "drupal/rootfs/var/www/drupal") + if got != projectDir { + t.Fatalf("resolveDrupalRoot() = %q, want %q", got, projectDir) + } +} + +func TestFormatListLinesWrapsThreePerLine(t *testing.T) { + values := []string{"action", "admin_toolbar", "big_pipe", "views", "pathauto", "token", "media"} + + got := formatListLines(values, 3) + want := []string{ + " action, admin_toolbar, big_pipe", + " views, pathauto, token", + " media", + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("formatListLines() = %v, want %v", got, want) + } +} + +func TestFormatListLinesEmptyReturnsNone(t *testing.T) { + got := formatListLines(nil, 3) + want := []string{" none"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("formatListLines() = %v, want %v", got, want) + } +} + +func TestParseFirstIntReadsFirstNonEmptyLine(t *testing.T) { + got, err := parseFirstInt("\n 1024 \nignored\n") + if err != nil { + t.Fatalf("parseFirstInt() error = %v", err) + } + if got != 1024 { + t.Fatalf("parseFirstInt() = %d, want 1024", got) + } +} + +func TestParseFirstIntErrorsWhenMissingNumber(t *testing.T) { + _, err := parseFirstInt("\n \n") + if err == nil { + t.Fatal("expected parseFirstInt() error") + } + if !strings.Contains(err.Error(), "no numeric output returned") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGetDrupalContainerForSDKRequiresSDK(t *testing.T) { + original := sdk + sdk = nil + defer func() { sdk = original }() + + _, _, _, err := getDrupalContainerForSDK(context.Background()) + if err == nil { + t.Fatal("expected getDrupalContainerForSDK() error") + } + if !strings.Contains(err.Error(), "plugin sdk is not initialized") { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestRenderDrupalDebugRequiresSDK(t *testing.T) { + original := sdk + sdk = nil + defer func() { sdk = original }() + + _, err := renderDrupalDebug(context.Background()) + if err == nil { + t.Fatal("expected renderDrupalDebug() error") + } + if !strings.Contains(err.Error(), "plugin sdk is not initialized") { + t.Fatalf("unexpected error: %v", err) + } +} diff --git a/cmd/helpers.go b/cmd/helpers.go index 94cb410..a40ede8 100644 --- a/cmd/helpers.go +++ b/cmd/helpers.go @@ -33,7 +33,7 @@ func getDrupalContainer(cmd *cobra.Command, args []string) (filteredArgs []strin } // Get the Drupal container name - containerName, err = cli.GetContainerName(ctx, *drupalServiceName) + containerName, err = cli.GetContainerNameContext(cmd.Context(), ctx, *drupalServiceName) if err != nil { cli.Close() return nil, nil, nil, "", err @@ -68,7 +68,7 @@ func getDrupalContainerFromFlags(cmd *cobra.Command) (ctx *config.Context, cli * } // Get the Drupal container name - containerName, err = cli.GetContainerName(ctx, *drupalServiceName) + containerName, err = cli.GetContainerNameContext(cmd.Context(), ctx, *drupalServiceName) if err != nil { cli.Close() return nil, nil, "", err diff --git a/cmd/root.go b/cmd/root.go index aeb8843..6f17363 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -19,6 +19,8 @@ func init() { func RegisterCommands(s *plugin.SDK) { sdk = s sdk.AddCommand(backupCmd) + sdk.AddCommand(componentExtensionCmd) + sdk.AddCommand(debugExtensionCmd) sdk.AddCommand(drushCmd) sdk.AddCommand(loginCmd) } diff --git a/cmd/uli.go b/cmd/uli.go index 3cd0647..b5ca92c 100644 --- a/cmd/uli.go +++ b/cmd/uli.go @@ -2,7 +2,6 @@ package cmd import ( "bytes" - "context" "fmt" "log/slog" "strings" @@ -39,7 +38,7 @@ Examples: var stdout, stderr bytes.Buffer drushCmd := []string{"bash", "-c", fmt.Sprintf("drush uli --uid=%d", uid)} - exitCode, err := cli.Exec(context.Background(), docker.ExecOptions{ + exitCode, err := cli.Exec(cmd.Context(), docker.ExecOptions{ Container: containerName, Cmd: drushCmd, AttachStdout: true, diff --git a/go.mod b/go.mod index 39de64b..1efc12e 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,34 @@ module github.com/libops/sitectl-drupal -go 1.25.3 +go 1.25.8 require ( + charm.land/lipgloss/v2 v2.0.2 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/libops/sitectl v0.3.1 + github.com/libops/sitectl v0.12.0 github.com/spf13/cobra v1.10.2 + golang.org/x/term v0.39.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( + charm.land/bubbles/v2 v2.0.0 // indirect + charm.land/bubbletea/v2 v2.0.2 // indirect + charm.land/fang/v2 v2.0.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/colorprofile v0.4.2 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260223171050-89c142e4aa73 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/distribution/reference v0.6.0 // indirect @@ -25,13 +41,23 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/joho/godotenv v1.5.1 // indirect github.com/kr/fs v0.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.0 // indirect + github.com/mattn/go-runewidth v0.0.20 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/mango v0.1.0 // indirect + github.com/muesli/mango-cobra v1.2.0 // indirect + github.com/muesli/mango-pflag v0.1.0 // indirect + github.com/muesli/roff v0.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/sftp v1.13.10 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/sahilm/fuzzy v0.1.1 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect go.opentelemetry.io/otel v1.39.0 // indirect @@ -40,7 +66,7 @@ require ( go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect golang.org/x/crypto v0.46.0 // indirect - golang.org/x/sys v0.40.0 // indirect - golang.org/x/term v0.39.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.32.0 // indirect ) diff --git a/go.sum b/go.sum index 81b3a60..b148654 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,43 @@ +charm.land/bubbles/v2 v2.0.0 h1:tE3eK/pHjmtrDiRdoC9uGNLgpopOd8fjhEe31B/ai5s= +charm.land/bubbles/v2 v2.0.0/go.mod h1:rCHoleP2XhU8um45NTuOWBPNVHxnkXKTiZqcclL/qOI= +charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= +charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= +charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY= +charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII= +charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= +charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= +github.com/charmbracelet/ultraviolet v0.0.0-20260223171050-89c142e4aa73 h1:Af/L28Xh+pddhouT/6lJ7IAIYfu5tWJOB0iqt+mXsYM= +github.com/charmbracelet/ultraviolet v0.0.0-20260223171050-89c142e4aa73/go.mod h1:E6/0abq9uG2SnM8IbLB9Y5SW09uIgfaFETk8aRzgXUQ= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -48,8 +80,14 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/libops/sitectl v0.3.1 h1:gBKod7DsoyGaGsdT5YjJHHV5zURFsQ1JWbzJ1V35Wag= -github.com/libops/sitectl v0.3.1/go.mod h1:U4FqA/Eb048TmqN/+fIDdMb9fKhHWrb2lLJrxuXwfJk= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/libops/sitectl v0.12.0 h1:xkpqskZnbfoZusyi0aVECRKluwwLDMYVcuForhMCMM0= +github.com/libops/sitectl v0.12.0/go.mod h1:QykPh7hrFKFBA1mp9euyKEWP1x5xB6bmEq6zT/tzeTU= +github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.20 h1:WcT52H91ZUAwy8+HUkdM3THM6gXqXuLJi9O3rjcQQaQ= +github.com/mattn/go-runewidth v0.0.20/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= @@ -60,6 +98,16 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= +github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= +github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= +github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= +github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= +github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= +github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= +github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -70,9 +118,13 @@ github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= @@ -82,6 +134,8 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= @@ -105,11 +159,15 @@ go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pq go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= diff --git a/scripts/use-go-work.sh b/scripts/use-go-work.sh new file mode 100755 index 0000000..60dbc23 --- /dev/null +++ b/scripts/use-go-work.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SITECTL_PATH="${1:-../sitectl}" +SITECTL_GOMOD="${SITECTL_PATH}/go.mod" + +if [[ ! -f "${SITECTL_GOMOD}" ]]; then + rm -f go.work + echo "Skipping go.work; local sitectl checkout not found at ${SITECTL_PATH}" + exit 0 +fi + +cat > go.work <