From 1d45b5d9c2a8d75b9c3e90d5ce588e8498f4ae34 Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Fri, 26 Dec 2025 14:50:21 -0500 Subject: [PATCH 1/2] public packages --- Dockerfile | 17 - cmd/internal/browse.go | 6 +- cmd/internal/cache.go | 4 +- cmd/internal/config.go | 6 +- cmd/internal/exec.go | 6 +- cmd/internal/flags/helpers.go | 2 +- cmd/internal/helpers.go | 6 +- cmd/internal/logs.go | 6 +- cmd/internal/mcp.go | 4 +- cmd/internal/secret.go | 4 +- cmd/internal/sync.go | 6 +- cmd/internal/template.go | 6 +- cmd/internal/vault.go | 6 +- cmd/internal/workspace.go | 8 +- cmd/root.go | 6 +- internal/fileparser/fileparser.go | 2 +- internal/fileparser/fileparser_test.go | 2 +- internal/io/cache/output.go | 2 +- internal/io/common/common.go | 2 +- internal/io/config/output.go | 2 +- internal/io/executable/output.go | 2 +- internal/io/executable/views.go | 4 +- internal/io/library/library.go | 2 +- internal/io/library/update.go | 4 +- internal/io/library/view.go | 2 +- internal/io/logs/output.go | 2 +- internal/io/secret/output.go | 4 +- internal/io/secret/views.go | 4 +- internal/io/vault/output.go | 2 +- internal/io/workspace/output.go | 2 +- internal/io/workspace/views.go | 4 +- internal/mcp/server_test.go | 2 +- internal/mcp/tools.go | 2 +- internal/runner/exec/exec.go | 4 +- internal/runner/launch/launch.go | 2 +- internal/runner/mocks/mock_runner.go | 2 +- internal/runner/parallel/parallel.go | 4 +- internal/runner/parallel/parallel_test.go | 2 +- internal/runner/render/render.go | 4 +- internal/runner/request/request.go | 4 +- internal/runner/runner.go | 4 +- internal/runner/runner_test.go | 2 +- internal/runner/serial/serial.go | 4 +- internal/runner/serial/serial_test.go | 2 +- internal/services/store/store.go | 3 +- internal/templates/artifacts.go | 4 +- internal/templates/form.go | 2 +- internal/templates/templates.go | 6 +- internal/utils/env/env.go | 4 +- internal/utils/env/env_test.go | 4 +- internal/utils/executables/executables.go | 2 +- internal/utils/utils.go | 2 +- internal/utils/utils_test.go | 2 +- internal/vault/vault.go | 4 +- main.go | 6 +- {internal => pkg}/cache/cache.go | 0 {internal => pkg}/cache/cache_test.go | 0 {internal => pkg}/cache/errors.go | 0 {internal => pkg}/cache/executables_cache.go | 6 +- .../cache/executables_cache_test.go | 8 +- .../cache/mocks/mock_executable_cache.go | 4 +- .../cache/mocks/mock_workspace_cache.go | 6 +- {internal => pkg}/cache/testdata/from-file.sh | 0 {internal => pkg}/cache/workspaces_cache.go | 6 +- .../cache/workspaces_cache_test.go | 6 +- pkg/cli/README.md | 401 ++++++++++++++++++ pkg/cli/builders.go | 215 ++++++++++ pkg/cli/builders_test.go | 219 ++++++++++ pkg/cli/doc.go | 65 +++ pkg/cli/examples/basic/main.go | 55 +++ pkg/cli/examples/hooks/main.go | 89 ++++ pkg/cli/examples/override/main.go | 106 +++++ pkg/cli/hooks.go | 245 +++++++++++ pkg/cli/hooks_test.go | 323 ++++++++++++++ pkg/cli/registry.go | 297 +++++++++++++ pkg/cli/registry_test.go | 293 +++++++++++++ pkg/cli/types.go | 79 ++++ {internal => pkg}/context/context.go | 6 +- {internal => pkg}/context/context_test.go | 0 {internal => pkg}/errors/errors.go | 0 {internal => pkg}/filesystem/cache.go | 0 {internal => pkg}/filesystem/cache_test.go | 3 +- {internal => pkg}/filesystem/config.go | 0 {internal => pkg}/filesystem/config_test.go | 2 +- {internal => pkg}/filesystem/executables.go | 2 +- .../filesystem/executables_test.go | 4 +- .../filesystem/filesystem_test.go | 0 {internal => pkg}/filesystem/helpers.go | 0 {internal => pkg}/filesystem/logs.go | 0 {internal => pkg}/filesystem/logs_test.go | 2 +- {internal => pkg}/filesystem/templates.go | 0 .../filesystem/templates_test.go | 2 +- {internal => pkg}/filesystem/workspace.go | 0 .../filesystem/workspace_test.go | 2 +- {internal => pkg}/logger/logger.go | 0 {internal => pkg}/logger/logger_test.go | 2 +- tests/utils/context.go | 10 +- tests/utils/runner.go | 2 +- tools/docsgen/main.go | 2 +- types/executable/executable.go | 2 +- 100 files changed, 2522 insertions(+), 154 deletions(-) delete mode 100644 Dockerfile rename {internal => pkg}/cache/cache.go (100%) rename {internal => pkg}/cache/cache_test.go (100%) rename {internal => pkg}/cache/errors.go (100%) rename {internal => pkg}/cache/executables_cache.go (98%) rename {internal => pkg}/cache/executables_cache_test.go (98%) rename {internal => pkg}/cache/mocks/mock_executable_cache.go (95%) rename {internal => pkg}/cache/mocks/mock_workspace_cache.go (94%) rename {internal => pkg}/cache/testdata/from-file.sh (100%) rename {internal => pkg}/cache/workspaces_cache.go (94%) rename {internal => pkg}/cache/workspaces_cache_test.go (94%) create mode 100644 pkg/cli/README.md create mode 100644 pkg/cli/builders.go create mode 100644 pkg/cli/builders_test.go create mode 100644 pkg/cli/doc.go create mode 100644 pkg/cli/examples/basic/main.go create mode 100644 pkg/cli/examples/hooks/main.go create mode 100644 pkg/cli/examples/override/main.go create mode 100644 pkg/cli/hooks.go create mode 100644 pkg/cli/hooks_test.go create mode 100644 pkg/cli/registry.go create mode 100644 pkg/cli/registry_test.go create mode 100644 pkg/cli/types.go rename {internal => pkg}/context/context.go (98%) rename {internal => pkg}/context/context_test.go (100%) rename {internal => pkg}/errors/errors.go (100%) rename {internal => pkg}/filesystem/cache.go (100%) rename {internal => pkg}/filesystem/cache_test.go (96%) rename {internal => pkg}/filesystem/config.go (100%) rename {internal => pkg}/filesystem/config_test.go (97%) rename {internal => pkg}/filesystem/executables.go (99%) rename {internal => pkg}/filesystem/executables_test.go (98%) rename {internal => pkg}/filesystem/filesystem_test.go (100%) rename {internal => pkg}/filesystem/helpers.go (100%) rename {internal => pkg}/filesystem/logs.go (100%) rename {internal => pkg}/filesystem/logs_test.go (92%) rename {internal => pkg}/filesystem/templates.go (100%) rename {internal => pkg}/filesystem/templates_test.go (98%) rename {internal => pkg}/filesystem/workspace.go (100%) rename {internal => pkg}/filesystem/workspace_test.go (96%) rename {internal => pkg}/logger/logger.go (100%) rename {internal => pkg}/logger/logger_test.go (92%) diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 04c53ba0..00000000 --- a/Dockerfile +++ /dev/null @@ -1,17 +0,0 @@ -FROM golang:1.25.4-bookworm - -ENV DISABLE_FLOW_INTERACTIVE="true" - -# TODO: replace with examples repo -ENV WORKSPACE="flow" -ENV REPO="https://github.com/flowexec/flow.git" -ENV BRANCH="" - -WORKDIR /workspaces -COPY flow /usr/bin/flow - -RUN if [ -z "$BRANCH" ]; then git clone $REPO .; else git clone -b $BRANCH $REPO .; fi -RUN flow workspace create $WORKSPACE . --set - -ENTRYPOINT ["flow"] -CMD ["--version"] \ No newline at end of file diff --git a/cmd/internal/browse.go b/cmd/internal/browse.go index 4d074147..d6141e3f 100644 --- a/cmd/internal/browse.go +++ b/cmd/internal/browse.go @@ -7,12 +7,12 @@ import ( "github.com/spf13/cobra" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/cache" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/io" execIO "github.com/flowexec/flow/internal/io/executable" "github.com/flowexec/flow/internal/io/library" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/cache" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/common" "github.com/flowexec/flow/types/executable" ) diff --git a/cmd/internal/cache.go b/cmd/internal/cache.go index ea82caa3..602407bf 100644 --- a/cmd/internal/cache.go +++ b/cmd/internal/cache.go @@ -8,11 +8,11 @@ import ( "github.com/spf13/cobra" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/context" flowIO "github.com/flowexec/flow/internal/io" cacheIO "github.com/flowexec/flow/internal/io/cache" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/services/store" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" ) func RegisterCacheCmd(ctx *context.Context, rootCmd *cobra.Command) { diff --git a/cmd/internal/config.go b/cmd/internal/config.go index a9d280cf..1a0f345a 100644 --- a/cmd/internal/config.go +++ b/cmd/internal/config.go @@ -12,11 +12,11 @@ import ( "github.com/spf13/cobra" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/filesystem" "github.com/flowexec/flow/internal/io" configIO "github.com/flowexec/flow/internal/io/config" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/config" ) diff --git a/cmd/internal/exec.go b/cmd/internal/exec.go index 3082f14f..641b10e7 100644 --- a/cmd/internal/exec.go +++ b/cmd/internal/exec.go @@ -14,10 +14,7 @@ import ( "github.com/spf13/cobra" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/cache" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/io" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/runner" "github.com/flowexec/flow/internal/runner/engine" "github.com/flowexec/flow/internal/runner/exec" @@ -28,6 +25,9 @@ import ( "github.com/flowexec/flow/internal/runner/serial" "github.com/flowexec/flow/internal/services/store" "github.com/flowexec/flow/internal/utils/env" + "github.com/flowexec/flow/pkg/cache" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" ) diff --git a/cmd/internal/flags/helpers.go b/cmd/internal/flags/helpers.go index 0e3f4f94..ac5e03fc 100644 --- a/cmd/internal/flags/helpers.go +++ b/cmd/internal/flags/helpers.go @@ -7,7 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/logger" ) //nolint:errcheck diff --git a/cmd/internal/helpers.go b/cmd/internal/helpers.go index c2370f9d..7b5c3b12 100644 --- a/cmd/internal/helpers.go +++ b/cmd/internal/helpers.go @@ -8,10 +8,10 @@ import ( "github.com/spf13/cobra" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/filesystem" flowIO "github.com/flowexec/flow/internal/io" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" ) diff --git a/cmd/internal/logs.go b/cmd/internal/logs.go index 665bab25..d2e9a005 100644 --- a/cmd/internal/logs.go +++ b/cmd/internal/logs.go @@ -8,10 +8,10 @@ import ( "github.com/spf13/cobra" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/filesystem" "github.com/flowexec/flow/internal/io/logs" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" ) func RegisterLogsCmd(ctx *context.Context, rootCmd *cobra.Command) { diff --git a/cmd/internal/mcp.go b/cmd/internal/mcp.go index 4668845b..7d85e14c 100644 --- a/cmd/internal/mcp.go +++ b/cmd/internal/mcp.go @@ -3,9 +3,9 @@ package internal import ( "github.com/spf13/cobra" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/mcp" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" ) func RegisterMCPCmd(ctx *context.Context, rootCmd *cobra.Command) { diff --git a/cmd/internal/secret.go b/cmd/internal/secret.go index 1aa53279..8e4f8e60 100644 --- a/cmd/internal/secret.go +++ b/cmd/internal/secret.go @@ -12,13 +12,13 @@ import ( "github.com/spf13/cobra" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/io" "github.com/flowexec/flow/internal/io/secret" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/utils" envUtils "github.com/flowexec/flow/internal/utils/env" "github.com/flowexec/flow/internal/vault" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/config" ) diff --git a/cmd/internal/sync.go b/cmd/internal/sync.go index 469038ec..d33bbe0d 100644 --- a/cmd/internal/sync.go +++ b/cmd/internal/sync.go @@ -3,9 +3,9 @@ package internal import ( "github.com/spf13/cobra" - "github.com/flowexec/flow/internal/cache" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/cache" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" ) func RegisterSyncCmd(ctx *context.Context, rootCmd *cobra.Command) { diff --git a/cmd/internal/template.go b/cmd/internal/template.go index 024e7895..1fc561be 100644 --- a/cmd/internal/template.go +++ b/cmd/internal/template.go @@ -6,13 +6,13 @@ import ( "github.com/spf13/cobra" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/filesystem" "github.com/flowexec/flow/internal/io/executable" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/runner" "github.com/flowexec/flow/internal/runner/exec" "github.com/flowexec/flow/internal/templates" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" ) func RegisterTemplateCmd(ctx *context.Context, rootCmd *cobra.Command) { diff --git a/cmd/internal/vault.go b/cmd/internal/vault.go index 7a93dea2..ae172af9 100644 --- a/cmd/internal/vault.go +++ b/cmd/internal/vault.go @@ -11,13 +11,13 @@ import ( "golang.org/x/exp/maps" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/filesystem" flowIO "github.com/flowexec/flow/internal/io" vaultIO "github.com/flowexec/flow/internal/io/vault" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/utils" "github.com/flowexec/flow/internal/vault" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/config" ) diff --git a/cmd/internal/workspace.go b/cmd/internal/workspace.go index d741f8c7..26b9ef11 100644 --- a/cmd/internal/workspace.go +++ b/cmd/internal/workspace.go @@ -13,12 +13,12 @@ import ( "golang.org/x/exp/maps" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/cache" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/filesystem" "github.com/flowexec/flow/internal/io" workspaceIO "github.com/flowexec/flow/internal/io/workspace" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/cache" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/common" "github.com/flowexec/flow/types/config" "github.com/flowexec/flow/types/workspace" diff --git a/cmd/root.go b/cmd/root.go index d42c183f..fab1978c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,9 +9,9 @@ import ( "github.com/flowexec/flow/cmd/internal" "github.com/flowexec/flow/cmd/internal/flags" "github.com/flowexec/flow/cmd/internal/version" - "github.com/flowexec/flow/internal/cache" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/cache" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" ) func NewRootCmd(ctx *context.Context) *cobra.Command { diff --git a/internal/fileparser/fileparser.go b/internal/fileparser/fileparser.go index e1d7c73b..b63b494f 100644 --- a/internal/fileparser/fileparser.go +++ b/internal/fileparser/fileparser.go @@ -6,8 +6,8 @@ import ( "path/filepath" "strings" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/utils" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/fileparser/fileparser_test.go b/internal/fileparser/fileparser_test.go index f21205fc..71b054bf 100644 --- a/internal/fileparser/fileparser_test.go +++ b/internal/fileparser/fileparser_test.go @@ -11,7 +11,7 @@ import ( "go.uber.org/mock/gomock" "github.com/flowexec/flow/internal/fileparser" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/io/cache/output.go b/internal/io/cache/output.go index e84a4d41..21bbf84f 100644 --- a/internal/io/cache/output.go +++ b/internal/io/cache/output.go @@ -2,7 +2,7 @@ package cache import ( "github.com/flowexec/flow/internal/io/common" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/logger" ) func PrintCache(cache map[string]string, format string) { diff --git a/internal/io/common/common.go b/internal/io/common/common.go index 10d0120e..0bbf73d8 100644 --- a/internal/io/common/common.go +++ b/internal/io/common/common.go @@ -6,8 +6,8 @@ import ( "slices" "strings" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/services/open" + "github.com/flowexec/flow/pkg/logger" ) const HeaderContextKey = "ctx" diff --git a/internal/io/config/output.go b/internal/io/config/output.go index 53c31149..1e863472 100644 --- a/internal/io/config/output.go +++ b/internal/io/config/output.go @@ -2,7 +2,7 @@ package config import ( "github.com/flowexec/flow/internal/io/common" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/config" ) diff --git a/internal/io/executable/output.go b/internal/io/executable/output.go index 949f3c39..923868f1 100644 --- a/internal/io/executable/output.go +++ b/internal/io/executable/output.go @@ -2,7 +2,7 @@ package executable import ( "github.com/flowexec/flow/internal/io/common" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/io/executable/views.go b/internal/io/executable/views.go index aa48764a..2acebc41 100644 --- a/internal/io/executable/views.go +++ b/internal/io/executable/views.go @@ -10,9 +10,9 @@ import ( "github.com/flowexec/tuikit/types" "github.com/flowexec/tuikit/views" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/io/common" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/io/library/library.go b/internal/io/library/library.go index c03ce3a7..6a013ea8 100644 --- a/internal/io/library/library.go +++ b/internal/io/library/library.go @@ -9,7 +9,7 @@ import ( "github.com/flowexec/tuikit/themes" "github.com/flowexec/tuikit/views" - "github.com/flowexec/flow/internal/context" + "github.com/flowexec/flow/pkg/context" "github.com/flowexec/flow/types/common" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" diff --git a/internal/io/library/update.go b/internal/io/library/update.go index 7166f97c..3d580c50 100644 --- a/internal/io/library/update.go +++ b/internal/io/library/update.go @@ -11,10 +11,10 @@ import ( "github.com/flowexec/tuikit/io" "github.com/flowexec/tuikit/themes" - "github.com/flowexec/flow/internal/filesystem" "github.com/flowexec/flow/internal/io/common" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/services/open" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" ) var log io.Logger diff --git a/internal/io/library/view.go b/internal/io/library/view.go index 71aca16e..6bd79e0e 100644 --- a/internal/io/library/view.go +++ b/internal/io/library/view.go @@ -10,7 +10,7 @@ import ( "github.com/flowexec/tuikit/themes" "github.com/jahvon/glamour" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/common" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" diff --git a/internal/io/logs/output.go b/internal/io/logs/output.go index 773a1ccd..bb0434ad 100644 --- a/internal/io/logs/output.go +++ b/internal/io/logs/output.go @@ -7,7 +7,7 @@ import ( "gopkg.in/yaml.v3" "github.com/flowexec/flow/internal/io/common" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/logger" ) type entry struct { diff --git a/internal/io/secret/output.go b/internal/io/secret/output.go index e4d7494a..03b6c813 100644 --- a/internal/io/secret/output.go +++ b/internal/io/secret/output.go @@ -3,10 +3,10 @@ package secret import ( "fmt" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/io/common" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/vault" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" ) func PrintSecrets(ctx *context.Context, vaultName string, vlt vault.Vault, format string, plaintext bool) { diff --git a/internal/io/secret/views.go b/internal/io/secret/views.go index 3509efe7..03d9e8ca 100644 --- a/internal/io/secret/views.go +++ b/internal/io/secret/views.go @@ -9,9 +9,9 @@ import ( "github.com/flowexec/tuikit/types" "github.com/flowexec/tuikit/views" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/logger" vault2 "github.com/flowexec/flow/internal/vault" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" ) func NewSecretView( diff --git a/internal/io/vault/output.go b/internal/io/vault/output.go index 68de68a8..d60a1ba8 100644 --- a/internal/io/vault/output.go +++ b/internal/io/vault/output.go @@ -2,7 +2,7 @@ package vault import ( "github.com/flowexec/flow/internal/io/common" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/logger" ) func PrintVault(format, vaultName string) { diff --git a/internal/io/workspace/output.go b/internal/io/workspace/output.go index 1a197e9f..87ccb7b2 100644 --- a/internal/io/workspace/output.go +++ b/internal/io/workspace/output.go @@ -2,7 +2,7 @@ package workspace import ( "github.com/flowexec/flow/internal/io/common" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/workspace" ) diff --git a/internal/io/workspace/views.go b/internal/io/workspace/views.go index 9808f6e9..d33a9e69 100644 --- a/internal/io/workspace/views.go +++ b/internal/io/workspace/views.go @@ -9,10 +9,10 @@ import ( "github.com/flowexec/tuikit/types" "github.com/flowexec/tuikit/views" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/filesystem" "github.com/flowexec/flow/internal/io/common" "github.com/flowexec/flow/internal/services/open" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" "github.com/flowexec/flow/types/workspace" ) diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go index 6e6d6360..42c7211e 100644 --- a/internal/mcp/server_test.go +++ b/internal/mcp/server_test.go @@ -10,9 +10,9 @@ import ( . "github.com/onsi/gomega" "go.uber.org/mock/gomock" - "github.com/flowexec/flow/internal/filesystem" flowMcp "github.com/flowexec/flow/internal/mcp" "github.com/flowexec/flow/internal/mcp/mocks" + "github.com/flowexec/flow/pkg/filesystem" ) func TestServer(t *testing.T) { diff --git a/internal/mcp/tools.go b/internal/mcp/tools.go index cbc50438..bf353190 100644 --- a/internal/mcp/tools.go +++ b/internal/mcp/tools.go @@ -12,7 +12,7 @@ import ( "github.com/mark3labs/mcp-go/server" "github.com/pkg/errors" - "github.com/flowexec/flow/internal/filesystem" + "github.com/flowexec/flow/pkg/filesystem" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/runner/exec/exec.go b/internal/runner/exec/exec.go index 47a38ee1..a6e8fac0 100644 --- a/internal/runner/exec/exec.go +++ b/internal/runner/exec/exec.go @@ -3,12 +3,12 @@ package exec import ( "github.com/pkg/errors" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/runner" "github.com/flowexec/flow/internal/runner/engine" "github.com/flowexec/flow/internal/services/run" "github.com/flowexec/flow/internal/utils/env" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/runner/launch/launch.go b/internal/runner/launch/launch.go index 5072a131..e6e5f875 100644 --- a/internal/runner/launch/launch.go +++ b/internal/runner/launch/launch.go @@ -6,12 +6,12 @@ import ( "github.com/pkg/errors" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/runner" "github.com/flowexec/flow/internal/runner/engine" "github.com/flowexec/flow/internal/services/open" "github.com/flowexec/flow/internal/utils" "github.com/flowexec/flow/internal/utils/env" + "github.com/flowexec/flow/pkg/context" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/runner/mocks/mock_runner.go b/internal/runner/mocks/mock_runner.go index bd949ebf..00db88ad 100644 --- a/internal/runner/mocks/mock_runner.go +++ b/internal/runner/mocks/mock_runner.go @@ -12,8 +12,8 @@ package mocks import ( reflect "reflect" - context "github.com/flowexec/flow/internal/context" engine "github.com/flowexec/flow/internal/runner/engine" + context "github.com/flowexec/flow/pkg/context" executable "github.com/flowexec/flow/types/executable" gomock "go.uber.org/mock/gomock" ) diff --git a/internal/runner/parallel/parallel.go b/internal/runner/parallel/parallel.go index 314c6d53..53819fef 100644 --- a/internal/runner/parallel/parallel.go +++ b/internal/runner/parallel/parallel.go @@ -12,13 +12,13 @@ import ( "github.com/pkg/errors" "golang.org/x/sync/errgroup" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/runner" "github.com/flowexec/flow/internal/runner/engine" "github.com/flowexec/flow/internal/services/store" envUtils "github.com/flowexec/flow/internal/utils/env" execUtils "github.com/flowexec/flow/internal/utils/executables" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/runner/parallel/parallel_test.go b/internal/runner/parallel/parallel_test.go index 43b33343..49cfc35e 100644 --- a/internal/runner/parallel/parallel_test.go +++ b/internal/runner/parallel/parallel_test.go @@ -9,11 +9,11 @@ import ( . "github.com/onsi/gomega" "go.uber.org/mock/gomock" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/runner" "github.com/flowexec/flow/internal/runner/engine" "github.com/flowexec/flow/internal/runner/engine/mocks" "github.com/flowexec/flow/internal/runner/parallel" + "github.com/flowexec/flow/pkg/context" testUtils "github.com/flowexec/flow/tests/utils" "github.com/flowexec/flow/tests/utils/builder" "github.com/flowexec/flow/types/executable" diff --git a/internal/runner/render/render.go b/internal/runner/render/render.go index 7c172ce2..6e6457a5 100644 --- a/internal/runner/render/render.go +++ b/internal/runner/render/render.go @@ -12,12 +12,12 @@ import ( "github.com/pkg/errors" "gopkg.in/yaml.v3" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/io" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/runner" "github.com/flowexec/flow/internal/runner/engine" "github.com/flowexec/flow/internal/utils/env" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/runner/request/request.go b/internal/runner/request/request.go index c213ff2f..6c00ac87 100644 --- a/internal/runner/request/request.go +++ b/internal/runner/request/request.go @@ -10,12 +10,12 @@ import ( "github.com/pkg/errors" "gopkg.in/yaml.v3" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/runner" "github.com/flowexec/flow/internal/runner/engine" "github.com/flowexec/flow/internal/services/rest" "github.com/flowexec/flow/internal/utils/env" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/runner/runner.go b/internal/runner/runner.go index d23f6e1c..635c42ff 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -7,9 +7,9 @@ import ( "github.com/jahvon/expression" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/runner/engine" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index 3cf8fd86..6186aa08 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -8,11 +8,11 @@ import ( . "github.com/onsi/gomega" "go.uber.org/mock/gomock" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/runner" "github.com/flowexec/flow/internal/runner/engine" engMocks "github.com/flowexec/flow/internal/runner/engine/mocks" "github.com/flowexec/flow/internal/runner/mocks" + "github.com/flowexec/flow/pkg/context" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/runner/serial/serial.go b/internal/runner/serial/serial.go index 9ca902ba..10df16ce 100644 --- a/internal/runner/serial/serial.go +++ b/internal/runner/serial/serial.go @@ -10,13 +10,13 @@ import ( "github.com/jahvon/expression" "github.com/pkg/errors" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/runner" "github.com/flowexec/flow/internal/runner/engine" "github.com/flowexec/flow/internal/services/store" envUtils "github.com/flowexec/flow/internal/utils/env" execUtils "github.com/flowexec/flow/internal/utils/executables" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/runner/serial/serial_test.go b/internal/runner/serial/serial_test.go index fe6d22a2..db2b0114 100644 --- a/internal/runner/serial/serial_test.go +++ b/internal/runner/serial/serial_test.go @@ -10,11 +10,11 @@ import ( . "github.com/onsi/gomega" "go.uber.org/mock/gomock" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/runner" "github.com/flowexec/flow/internal/runner/engine" "github.com/flowexec/flow/internal/runner/engine/mocks" "github.com/flowexec/flow/internal/runner/serial" + "github.com/flowexec/flow/pkg/context" testUtils "github.com/flowexec/flow/tests/utils" "github.com/flowexec/flow/tests/utils/builder" "github.com/flowexec/flow/types/executable" diff --git a/internal/services/store/store.go b/internal/services/store/store.go index 55d25c95..bec6991e 100644 --- a/internal/services/store/store.go +++ b/internal/services/store/store.go @@ -7,11 +7,10 @@ import ( "strings" "time" + "github.com/flowexec/flow/pkg/filesystem" "github.com/pkg/errors" bolt "go.etcd.io/bbolt" boltErrors "go.etcd.io/bbolt/errors" - - "github.com/flowexec/flow/internal/filesystem" ) const ( diff --git a/internal/templates/artifacts.go b/internal/templates/artifacts.go index 2ddfcef3..0f43ec05 100644 --- a/internal/templates/artifacts.go +++ b/internal/templates/artifacts.go @@ -10,8 +10,8 @@ import ( "github.com/jahvon/expression" "github.com/pkg/errors" - "github.com/flowexec/flow/internal/filesystem" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/templates/form.go b/internal/templates/form.go index 70aef8e8..6f945076 100644 --- a/internal/templates/form.go +++ b/internal/templates/form.go @@ -5,8 +5,8 @@ import ( "github.com/flowexec/tuikit/views" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/io" + "github.com/flowexec/flow/pkg/context" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/templates/templates.go b/internal/templates/templates.go index c186f1df..4724eb0c 100644 --- a/internal/templates/templates.go +++ b/internal/templates/templates.go @@ -13,14 +13,14 @@ import ( "github.com/pkg/errors" "gopkg.in/yaml.v3" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/filesystem" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/runner" "github.com/flowexec/flow/internal/runner/engine" "github.com/flowexec/flow/internal/utils" argUtils "github.com/flowexec/flow/internal/utils/env" execUtils "github.com/flowexec/flow/internal/utils/executables" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" ) diff --git a/internal/utils/env/env.go b/internal/utils/env/env.go index 8c305125..1c47900e 100644 --- a/internal/utils/env/env.go +++ b/internal/utils/env/env.go @@ -7,10 +7,10 @@ import ( "path/filepath" "strings" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/filesystem" "github.com/flowexec/flow/internal/io" "github.com/flowexec/flow/internal/utils" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/utils/env/env_test.go b/internal/utils/env/env_test.go index f8012651..90b18baf 100644 --- a/internal/utils/env/env_test.go +++ b/internal/utils/env/env_test.go @@ -10,10 +10,10 @@ import ( . "github.com/onsi/gomega" "go.uber.org/mock/gomock" - "github.com/flowexec/flow/internal/context" "github.com/flowexec/flow/internal/io" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/utils/env" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/config" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" diff --git a/internal/utils/executables/executables.go b/internal/utils/executables/executables.go index ab308c0d..bc01a653 100644 --- a/internal/utils/executables/executables.go +++ b/internal/utils/executables/executables.go @@ -3,7 +3,7 @@ package executables import ( "fmt" - "github.com/flowexec/flow/internal/context" + "github.com/flowexec/flow/pkg/context" "github.com/flowexec/flow/types/common" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/utils/utils.go b/internal/utils/utils.go index d12aff0c..ce32c8c0 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -10,7 +10,7 @@ import ( "github.com/pkg/errors" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/logger" ) // ExpandPath expands a general path to an absolute path with security validation. diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index b8e04581..61430e3e 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -10,8 +10,8 @@ import ( . "github.com/onsi/gomega" "go.uber.org/mock/gomock" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/utils" + "github.com/flowexec/flow/pkg/logger" ) func TestUtils(t *testing.T) { diff --git a/internal/vault/vault.go b/internal/vault/vault.go index 1d0775d3..a8eac540 100644 --- a/internal/vault/vault.go +++ b/internal/vault/vault.go @@ -8,9 +8,9 @@ import ( "github.com/flowexec/vault" - "github.com/flowexec/flow/internal/filesystem" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/utils" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" ) const ( diff --git a/main.go b/main.go index fb8eed6e..fbe2cf59 100644 --- a/main.go +++ b/main.go @@ -8,10 +8,10 @@ import ( "slices" "github.com/flowexec/flow/cmd" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/filesystem" "github.com/flowexec/flow/internal/io" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) diff --git a/internal/cache/cache.go b/pkg/cache/cache.go similarity index 100% rename from internal/cache/cache.go rename to pkg/cache/cache.go diff --git a/internal/cache/cache_test.go b/pkg/cache/cache_test.go similarity index 100% rename from internal/cache/cache_test.go rename to pkg/cache/cache_test.go diff --git a/internal/cache/errors.go b/pkg/cache/errors.go similarity index 100% rename from internal/cache/errors.go rename to pkg/cache/errors.go diff --git a/internal/cache/executables_cache.go b/pkg/cache/executables_cache.go similarity index 98% rename from internal/cache/executables_cache.go rename to pkg/cache/executables_cache.go index de036916..97d1e5d7 100644 --- a/internal/cache/executables_cache.go +++ b/pkg/cache/executables_cache.go @@ -7,8 +7,8 @@ import ( "gopkg.in/yaml.v3" "github.com/flowexec/flow/internal/fileparser" - "github.com/flowexec/flow/internal/filesystem" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/common" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" @@ -16,7 +16,7 @@ import ( const execCacheKey = "executables" -//go:generate mockgen -destination=mocks/mock_executable_cache.go -package=mocks github.com/flowexec/flow/internal/cache ExecutableCache +//go:generate mockgen -destination=mocks/mock_executable_cache.go -package=mocks github.com/flowexec/flow/pkg/cache ExecutableCache type ExecutableCache interface { Update() error GetExecutableByRef(ref executable.Ref) (*executable.Executable, error) diff --git a/internal/cache/executables_cache_test.go b/pkg/cache/executables_cache_test.go similarity index 98% rename from internal/cache/executables_cache_test.go rename to pkg/cache/executables_cache_test.go index 7611459c..a5bfc99b 100644 --- a/internal/cache/executables_cache_test.go +++ b/pkg/cache/executables_cache_test.go @@ -10,10 +10,10 @@ import ( . "github.com/onsi/gomega" "go.uber.org/mock/gomock" - "github.com/flowexec/flow/internal/cache" - cacheMocks "github.com/flowexec/flow/internal/cache/mocks" - "github.com/flowexec/flow/internal/filesystem" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/cache" + cacheMocks "github.com/flowexec/flow/pkg/cache/mocks" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/common" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" diff --git a/internal/cache/mocks/mock_executable_cache.go b/pkg/cache/mocks/mock_executable_cache.go similarity index 95% rename from internal/cache/mocks/mock_executable_cache.go rename to pkg/cache/mocks/mock_executable_cache.go index 5c3f84b4..ada6bbaa 100644 --- a/internal/cache/mocks/mock_executable_cache.go +++ b/pkg/cache/mocks/mock_executable_cache.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/flowexec/flow/internal/cache (interfaces: ExecutableCache) +// Source: github.com/flowexec/flow/pkg/cache (interfaces: ExecutableCache) // // Generated by this command: // -// mockgen -destination=mocks/mock_executable_cache.go -package=mocks github.com/flowexec/flow/internal/cache ExecutableCache +// mockgen -destination=mocks/mock_executable_cache.go -package=mocks github.com/flowexec/flow/pkg/cache ExecutableCache // // Package mocks is a generated GoMock package. diff --git a/internal/cache/mocks/mock_workspace_cache.go b/pkg/cache/mocks/mock_workspace_cache.go similarity index 94% rename from internal/cache/mocks/mock_workspace_cache.go rename to pkg/cache/mocks/mock_workspace_cache.go index ed75b3c7..076c0e43 100644 --- a/internal/cache/mocks/mock_workspace_cache.go +++ b/pkg/cache/mocks/mock_workspace_cache.go @@ -1,9 +1,9 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/flowexec/flow/internal/cache (interfaces: WorkspaceCache) +// Source: github.com/flowexec/flow/pkg/cache (interfaces: WorkspaceCache) // // Generated by this command: // -// mockgen -destination=mocks/mock_workspace_cache.go -package=mocks github.com/flowexec/flow/internal/cache WorkspaceCache +// mockgen -destination=mocks/mock_workspace_cache.go -package=mocks github.com/flowexec/flow/pkg/cache WorkspaceCache // // Package mocks is a generated GoMock package. @@ -12,7 +12,7 @@ package mocks import ( reflect "reflect" - cache "github.com/flowexec/flow/internal/cache" + cache "github.com/flowexec/flow/pkg/cache" workspace "github.com/flowexec/flow/types/workspace" gomock "go.uber.org/mock/gomock" ) diff --git a/internal/cache/testdata/from-file.sh b/pkg/cache/testdata/from-file.sh similarity index 100% rename from internal/cache/testdata/from-file.sh rename to pkg/cache/testdata/from-file.sh diff --git a/internal/cache/workspaces_cache.go b/pkg/cache/workspaces_cache.go similarity index 94% rename from internal/cache/workspaces_cache.go rename to pkg/cache/workspaces_cache.go index b6f5d5ae..64dc796c 100644 --- a/internal/cache/workspaces_cache.go +++ b/pkg/cache/workspaces_cache.go @@ -4,14 +4,14 @@ import ( "github.com/pkg/errors" "gopkg.in/yaml.v3" - "github.com/flowexec/flow/internal/filesystem" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/workspace" ) const wsCacheKey = "workspace" -//go:generate mockgen -destination=mocks/mock_workspace_cache.go -package=mocks github.com/flowexec/flow/internal/cache WorkspaceCache +//go:generate mockgen -destination=mocks/mock_workspace_cache.go -package=mocks github.com/flowexec/flow/pkg/cache WorkspaceCache type WorkspaceCache interface { Update() error GetData() *WorkspaceCacheData diff --git a/internal/cache/workspaces_cache_test.go b/pkg/cache/workspaces_cache_test.go similarity index 94% rename from internal/cache/workspaces_cache_test.go rename to pkg/cache/workspaces_cache_test.go index d2b308a4..056036b5 100644 --- a/internal/cache/workspaces_cache_test.go +++ b/pkg/cache/workspaces_cache_test.go @@ -8,9 +8,9 @@ import ( . "github.com/onsi/gomega" "go.uber.org/mock/gomock" - "github.com/flowexec/flow/internal/cache" - "github.com/flowexec/flow/internal/filesystem" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/cache" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/workspace" ) diff --git a/pkg/cli/README.md b/pkg/cli/README.md new file mode 100644 index 00000000..75d8cfcb --- /dev/null +++ b/pkg/cli/README.md @@ -0,0 +1,401 @@ +# Flow CLI Extension API + +The `pkg/cli` package provides a public API for extending and customizing the Flow CLI. This allows external projects to build custom CLIs that include Flow's commands, add cross-cutting functionality, and override existing behavior. + +## Table of Contents + +- [Features](#features) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [API Overview](#api-overview) + - [Command Builders](#command-builders) + - [Hook Injection](#hook-injection) + - [Command Registry](#command-registry) + - [Root Command Customization](#root-command-customization) +- [Examples](#examples) +- [API Reference](#api-reference) +- [Best Practices](#best-practices) + +## Features + +- **Command Builders**: Create Flow commands programmatically +- **Hook Injection**: Add PreRun/PostRun hooks to any command +- **Command Registry**: Manipulate the command tree (find, replace, remove) +- **Root Customization**: Customize the root command with functional options +- **Backward Compatible**: Works alongside standard Flow CLI + +## Installation + +```bash +go get github.com/flowexec/flow +``` + +Then import the package: + +```go +import "github.com/flowexec/flow/pkg/cli" +``` + +## Quick Start + +Here's a minimal example of building a custom Flow CLI: + +```go +package main + +import ( + stdCtx "context" + + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filessystem" + "github.com/flowexec/flow/internal/io" + "github.com/flowexec/flow/pkg/logger" + "github.com/flowexec/flow/pkg/cli" +) + +func main() { + // Setup (same as standard Flow CLI) + cfg, _ := filesystem.LoadConfig() + logger.Init(logger.InitOptions{ + StdOut: io.Stdout, + LogMode: cfg.DefaultLogMode, + Theme: io.Theme(cfg.Theme.String()), + }) + defer logger.Log().Flush() + + // Create context + bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) + ctx := context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) + defer ctx.Finalize() + + // Build custom CLI + rootCmd := cli.BuildRootCommand(ctx, + cli.WithVersion("1.0.0-custom"), + ) + cli.RegisterAllCommands(ctx, rootCmd) + + // Execute + cli.Execute(ctx, rootCmd) +} +``` + +## API Overview + +### Command Builders + +Create Flow commands programmatically: + +```go +// Build root command +rootCmd := cli.BuildRootCommand(ctx, + cli.WithVersion("1.0.0"), + cli.WithShort("My custom CLI"), +) + +// Register all commands at once +cli.RegisterAllCommands(ctx, rootCmd) + +// Or build individual commands +execCmd := cli.BuildExecCommand(ctx) +wsCmd := cli.BuildWorkspaceCommand(ctx) +rootCmd.AddCommand(execCmd, wsCmd) +``` + +Available command builders: +- `BuildRootCommand(ctx, ...opts)` - Root command +- `BuildExecCommand(ctx)` - Exec command +- `BuildBrowseCommand(ctx)` - Browse command +- `BuildConfigCommand(ctx)` - Config command +- `BuildSecretCommand(ctx)` - Secret command +- `BuildVaultCommand(ctx)` - Vault command +- `BuildCacheCommand(ctx)` - Cache command +- `BuildWorkspaceCommand(ctx)` - Workspace command +- `BuildTemplateCommand(ctx)` - Template command +- `BuildLogsCommand(ctx)` - Logs command +- `BuildSyncCommand(ctx)` - Sync command +- `BuildMCPCommand(ctx)` - MCP command + +### Hook Injection + +Add cross-cutting functionality to commands: + +```go +// Add hooks to a single command +cli.AddPreRunHook(cmd, func(cmd *cobra.Command, args []string) { + log.Println("Before command:", cmd.Name()) +}) + +cli.AddPostRunHook(cmd, func(cmd *cobra.Command, args []string) { + log.Println("After command:", cmd.Name()) +}) + +// Add hooks to all commands recursively +cli.ApplyHooksRecursive(rootCmd, + // PreRun hook + func(cmd *cobra.Command, args []string) { + telemetry.Start(cmd.Name()) + }, + // PostRun hook + func(cmd *cobra.Command, args []string) { + telemetry.End(cmd.Name()) + }, +) + +// Add persistent hooks (inherited by subcommands) +cli.AddPersistentPreRunHook(rootCmd, initHook) +cli.AddPersistentPostRunHook(rootCmd, cleanupHook) +``` + +Hook functions: +- `AddPreRunHook(cmd, hook)` - Add PreRun hook +- `AddPostRunHook(cmd, hook)` - Add PostRun hook +- `AddPersistentPreRunHook(cmd, hook)` - Add PersistentPreRun hook +- `AddPersistentPostRunHook(cmd, hook)` - Add PersistentPostRun hook +- `ApplyHooksRecursive(cmd, preRun, postRun)` - Apply to entire tree +- `ApplyPersistentHooksRecursive(cmd, preRun, postRun)` - Apply persistent to tree + +### Command Registry + +Manipulate the command tree: + +```go +// Find a command +wsCmd := cli.FindCommand(rootCmd, "workspace") + +// Find by path +addCmd := cli.FindCommandPath(rootCmd, "workspace add") + +// Replace a command +customExec := &cobra.Command{...} +cli.ReplaceCommand(rootCmd, "exec", customExec) + +// Remove a command +cli.RemoveCommand(rootCmd, "sync") + +// Walk all commands +cli.WalkCommands(rootCmd, func(cmd *cobra.Command) { + fmt.Println("Command:", cmd.Name()) +}) + +// List all command names +names := cli.ListCommands(rootCmd) + +// Get subcommands +subCmds := cli.GetSubcommands(wsCmd) +``` + +Registry functions: +- `WalkCommands(root, fn)` - Traverse command tree +- `FindCommand(root, name)` - Find command by name +- `FindCommandPath(root, path)` - Find by full path +- `ReplaceCommand(root, name, newCmd)` - Replace command +- `RemoveCommand(root, name)` - Remove command +- `ListCommands(root)` - Get all command names +- `GetSubcommands(cmd)` - Get subcommands map +- `CloneCommand(cmd)` - Clone a command + +### Root Command Customization + +Use functional options to customize the root command: + +```go +rootCmd := cli.BuildRootCommand(ctx, + cli.WithUse("mycli"), + cli.WithShort("My custom CLI description"), + cli.WithLong("Detailed description..."), + cli.WithVersion("1.0.0"), + cli.WithPersistentPreRun(func(cmd *cobra.Command, args []string) { + // Global initialization + }), + cli.WithPersistentPostRun(func(cmd *cobra.Command, args []string) { + // Global cleanup + }), +) +``` + +Available options: +- `WithUse(use)` - Set Use field +- `WithShort(short)` - Set Short description +- `WithLong(long)` - Set Long description +- `WithVersion(version)` - Set Version string +- `WithPersistentPreRun(hook)` - Set PersistentPreRun hook +- `WithPersistentPostRun(hook)` - Set PersistentPostRun hook + +## Examples + +See the [examples](./examples) directory for complete working examples: + +- **[basic](./examples/basic/main.go)** - Basic CLI with custom version +- **[hooks](./examples/hooks/main.go)** - Adding telemetry hooks to all commands +- **[override](./examples/override/main.go)** - Overriding commands and adding new ones + +## API Reference + +### Types + +```go +// HookFunc is a function that can be used as a PreRun or PostRun hook +type HookFunc func(cmd *cobra.Command, args []string) + +// RootConfig holds configuration options for building the root command +type RootConfig struct { + Use string + Short string + Long string + Version string + PersistentPreRun HookFunc + PersistentPostRun HookFunc +} + +// RootOption is a functional option for configuring the root command +type RootOption func(*RootConfig) +``` + +### Core Functions + +```go +// Build and execute +BuildRootCommand(ctx *context.Context, opts ...RootOption) *cobra.Command +RegisterAllCommands(ctx *context.Context, rootCmd *cobra.Command) +Execute(ctx *context.Context, rootCmd *cobra.Command) error + +// Command builders +BuildExecCommand(ctx *context.Context) *cobra.Command +BuildBrowseCommand(ctx *context.Context) *cobra.Command +BuildConfigCommand(ctx *context.Context) *cobra.Command +BuildSecretCommand(ctx *context.Context) *cobra.Command +BuildVaultCommand(ctx *context.Context) *cobra.Command +BuildCacheCommand(ctx *context.Context) *cobra.Command +BuildWorkspaceCommand(ctx *context.Context) *cobra.Command +BuildTemplateCommand(ctx *context.Context) *cobra.Command +BuildLogsCommand(ctx *context.Context) *cobra.Command +BuildSyncCommand(ctx *context.Context) *cobra.Command +BuildMCPCommand(ctx *context.Context) *cobra.Command + +// Hook injection +AddPreRunHook(cmd *cobra.Command, hook HookFunc) +AddPostRunHook(cmd *cobra.Command, hook HookFunc) +AddPersistentPreRunHook(cmd *cobra.Command, hook HookFunc) +AddPersistentPostRunHook(cmd *cobra.Command, hook HookFunc) +ApplyHooksRecursive(cmd *cobra.Command, preRun, postRun HookFunc) +ApplyPersistentHooksRecursive(cmd *cobra.Command, preRun, postRun HookFunc) +WrapRunFunc(cmd *cobra.Command, before, after HookFunc) +WrapRunEFunc(cmd *cobra.Command, before, after HookFunc) + +// Command registry +WalkCommands(rootCmd *cobra.Command, fn func(*cobra.Command)) +FindCommand(rootCmd *cobra.Command, name string) *cobra.Command +FindCommandPath(rootCmd *cobra.Command, path string) *cobra.Command +ReplaceCommand(rootCmd *cobra.Command, oldName string, newCmd *cobra.Command) error +RemoveCommand(rootCmd *cobra.Command, name string) error +ListCommands(rootCmd *cobra.Command) []string +GetSubcommands(cmd *cobra.Command) map[string]*cobra.Command +CloneCommand(cmd *cobra.Command) *cobra.Command + +// Root options +WithUse(use string) RootOption +WithShort(short string) RootOption +WithLong(long string) RootOption +WithVersion(version string) RootOption +WithPersistentPreRun(hook HookFunc) RootOption +WithPersistentPostRun(hook HookFunc) RootOption +``` + +## Best Practices + +### 1. Hook Ordering + +Hooks are chained in the order they're added: +- **PreRun**: New hooks run before existing hooks +- **PostRun**: New hooks run after existing hooks + +```go +// This hook runs first +cli.AddPreRunHook(cmd, hook1) +// This hook runs second +cli.AddPreRunHook(cmd, hook2) +``` + +### 2. Context Management + +Always create and finalize the Flow context properly: + +```go +bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) +ctx := context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) +defer ctx.Finalize() // Important! +``` + +### 3. Command Replacement + +When replacing commands, ensure the new command has the same `Use` field: + +```go +// Bad: Use field doesn't match +newCmd := &cobra.Command{Use: "execute", ...} +cli.ReplaceCommand(rootCmd, "exec", newCmd) // Won't work as expected + +// Good: Use field matches +newCmd := &cobra.Command{Use: "exec", ...} +cli.ReplaceCommand(rootCmd, "exec", newCmd) // Works correctly +``` + +### 4. Error Handling + +Always check errors from registry operations: + +```go +if err := cli.ReplaceCommand(rootCmd, "exec", newCmd); err != nil { + log.Fatalf("Failed to replace command: %v", err) +} +``` + +### 5. Hook Safety + +Hooks should be safe to call multiple times and handle nil values: + +```go +// Good: Safe hook implementation +myHook := func(cmd *cobra.Command, args []string) { + if cmd == nil { + return + } + // Do work... +} +``` + +### 6. Building Individual Commands + +When building individual commands instead of using `RegisterAllCommands`, remember that some commands may have dependencies: + +```go +// Build commands with potential dependencies +execCmd := cli.BuildExecCommand(ctx) +cacheCmd := cli.BuildCacheCommand(ctx) // Exec may depend on cache + +rootCmd.AddCommand(execCmd, cacheCmd) +``` + +## Thread Safety + +This package is **not thread-safe**. Command building and modification should be done during CLI initialization, not concurrently. + +## Versioning + +This package follows semantic versioning: +- **Major**: Breaking changes to the public API +- **Minor**: New features, backward compatible +- **Patch**: Bug fixes, backward compatible + +## Contributing + +When contributing to this package: +1. Maintain backward compatibility +2. Add comprehensive godoc comments +3. Include examples for new features +4. Write tests for all public functions +5. Update this README with new features + +## License + +This package is part of the Flow CLI project and follows the same license. diff --git a/pkg/cli/builders.go b/pkg/cli/builders.go new file mode 100644 index 00000000..fca53ab9 --- /dev/null +++ b/pkg/cli/builders.go @@ -0,0 +1,215 @@ +package cli + +import ( + "github.com/spf13/cobra" + + "github.com/flowexec/flow/cmd" + "github.com/flowexec/flow/pkg/context" +) + +// BuildRootCommand creates a new root command with optional configuration. +// The root command is the main entry point for the CLI. +// +// By default, this creates a root command with Flow's standard configuration. +// Use RootOption functions to customize the command. +// +// Example: +// +// rootCmd := cli.BuildRootCommand(ctx, +// cli.WithVersion("1.0.0-custom"), +// cli.WithShort("My custom Flow CLI"), +// ) +func BuildRootCommand(ctx *context.Context, opts ...RootOption) *cobra.Command { + // Create the root command using Flow's standard builder + rootCmd := cmd.NewRootCmd(ctx) + + // Apply any custom configuration + if len(opts) > 0 { + config := &RootConfig{} + for _, opt := range opts { + opt(config) + } + + if config.Use != "" { + rootCmd.Use = config.Use + } + if config.Short != "" { + rootCmd.Short = config.Short + } + if config.Long != "" { + rootCmd.Long = config.Long + } + if config.Version != "" { + rootCmd.Version = config.Version + } + if config.PersistentPreRun != nil { + rootCmd.PersistentPreRun = config.PersistentPreRun + } + if config.PersistentPostRun != nil { + rootCmd.PersistentPostRun = config.PersistentPostRun + } + } + + return rootCmd +} + +// RegisterAllCommands registers all Flow commands to the root command. +// This includes: exec, browse, config, secret, vault, cache, workspace, template, logs, sync, mcp. +// +// This is a convenience function that calls all the individual Register*Command functions. +// +// Example: +// +// rootCmd := cli.BuildRootCommand(ctx) +// cli.RegisterAllCommands(ctx, rootCmd) +func RegisterAllCommands(ctx *context.Context, rootCmd *cobra.Command) { + cmd.RegisterSubCommands(ctx, rootCmd) +} + +// Execute runs the root command. This is a convenience wrapper around cobra's Execute. +// It configures the command's IO streams and executes it. +// +// Example: +// +// rootCmd := cli.BuildRootCommand(ctx) +// cli.RegisterAllCommands(ctx, rootCmd) +// if err := cli.Execute(ctx, rootCmd); err != nil { +// log.Fatal(err) +// } +func Execute(ctx *context.Context, rootCmd *cobra.Command) error { + return cmd.Execute(ctx, rootCmd) +} + +// BuildExecCommand creates the exec command with all its configuration. +// The exec command is used to execute workflows by their verb and reference. +// +// This command is typically registered automatically via RegisterAllCommands, +// but can be built separately for customization or replacement. +// +// Note: Individual command builders require commands to be registered to a parent +// first, so this creates a temporary parent and returns the registered command. +// +// Example: +// +// execCmd := cli.BuildExecCommand(ctx) +// // Customize execCmd... +// rootCmd.AddCommand(execCmd) +func BuildExecCommand(ctx *context.Context) *cobra.Command { + return buildCommand(ctx, "exec") +} + +// BuildBrowseCommand creates the browse command for browsing executables. +// +// Example: +// +// browseCmd := cli.BuildBrowseCommand(ctx) +// rootCmd.AddCommand(browseCmd) +func BuildBrowseCommand(ctx *context.Context) *cobra.Command { + return buildCommand(ctx, "browse") +} + +// BuildConfigCommand creates the config command for managing Flow configuration. +// +// Example: +// +// configCmd := cli.BuildConfigCommand(ctx) +// rootCmd.AddCommand(configCmd) +func BuildConfigCommand(ctx *context.Context) *cobra.Command { + return buildCommand(ctx, "config") +} + +// BuildSecretCommand creates the secret command for managing secrets. +// +// Example: +// +// secretCmd := cli.BuildSecretCommand(ctx) +// rootCmd.AddCommand(secretCmd) +func BuildSecretCommand(ctx *context.Context) *cobra.Command { + return buildCommand(ctx, "secret") +} + +// BuildVaultCommand creates the vault command for managing the secrets vault. +// +// Example: +// +// vaultCmd := cli.BuildVaultCommand(ctx) +// rootCmd.AddCommand(vaultCmd) +func BuildVaultCommand(ctx *context.Context) *cobra.Command { + return buildCommand(ctx, "vault") +} + +// BuildCacheCommand creates the cache command for managing Flow's cache. +// +// Example: +// +// cacheCmd := cli.BuildCacheCommand(ctx) +// rootCmd.AddCommand(cacheCmd) +func BuildCacheCommand(ctx *context.Context) *cobra.Command { + return buildCommand(ctx, "cache") +} + +// BuildWorkspaceCommand creates the workspace command for managing workspaces. +// +// Example: +// +// wsCmd := cli.BuildWorkspaceCommand(ctx) +// rootCmd.AddCommand(wsCmd) +func BuildWorkspaceCommand(ctx *context.Context) *cobra.Command { + return buildCommand(ctx, "workspace") +} + +// BuildTemplateCommand creates the template command for managing templates. +// +// Example: +// +// templateCmd := cli.BuildTemplateCommand(ctx) +// rootCmd.AddCommand(templateCmd) +func BuildTemplateCommand(ctx *context.Context) *cobra.Command { + return buildCommand(ctx, "template") +} + +// BuildLogsCommand creates the logs command for viewing execution logs. +// +// Example: +// +// logsCmd := cli.BuildLogsCommand(ctx) +// rootCmd.AddCommand(logsCmd) +func BuildLogsCommand(ctx *context.Context) *cobra.Command { + return buildCommand(ctx, "logs") +} + +// BuildSyncCommand creates the sync command for synchronizing caches. +// +// Example: +// +// syncCmd := cli.BuildSyncCommand(ctx) +// rootCmd.AddCommand(syncCmd) +func BuildSyncCommand(ctx *context.Context) *cobra.Command { + return buildCommand(ctx, "sync") +} + +// BuildMCPCommand creates the mcp command for MCP server management. +// +// Example: +// +// mcpCmd := cli.BuildMCPCommand(ctx) +// rootCmd.AddCommand(mcpCmd) +func BuildMCPCommand(ctx *context.Context) *cobra.Command { + return buildCommand(ctx, "mcp") +} + +// buildCommand is a helper that creates a temporary root, registers all commands, +// and returns the requested command. This is necessary because the cmd package +// uses internal registration functions that we can't access directly. +func buildCommand(ctx *context.Context, name string) *cobra.Command { + tempRoot := &cobra.Command{} + cmd.RegisterSubCommands(ctx, tempRoot) + + // Find and return the command with the matching name + for _, c := range tempRoot.Commands() { + if c.Name() == name { + return c + } + } + return nil +} diff --git a/pkg/cli/builders_test.go b/pkg/cli/builders_test.go new file mode 100644 index 00000000..16417484 --- /dev/null +++ b/pkg/cli/builders_test.go @@ -0,0 +1,219 @@ +package cli_test + +import ( + stdCtx "context" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/flowexec/flow/internal/io" + "github.com/flowexec/flow/pkg/cli" + "github.com/flowexec/flow/pkg/context" +) + +func TestBuilders(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CLI Builders Suite") +} + +var _ = Describe("BuildRootCommand", func() { + var ctx *context.Context + + BeforeEach(func() { + bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) + ctx = context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) + }) + + AfterEach(func() { + if ctx != nil { + ctx.Finalize() + } + }) + + It("should create a root command with default configuration", func() { + rootCmd := cli.BuildRootCommand(ctx) + + Expect(rootCmd).NotTo(BeNil()) + Expect(rootCmd.Use).To(Equal("flow")) + Expect(rootCmd.Short).NotTo(BeEmpty()) + }) + + It("should apply custom use", func() { + rootCmd := cli.BuildRootCommand(ctx, cli.WithUse("mycli")) + + Expect(rootCmd.Use).To(Equal("mycli")) + }) + + It("should apply custom short description", func() { + rootCmd := cli.BuildRootCommand(ctx, cli.WithShort("Custom short")) + + Expect(rootCmd.Short).To(Equal("Custom short")) + }) + + It("should apply custom long description", func() { + rootCmd := cli.BuildRootCommand(ctx, cli.WithLong("Custom long")) + + Expect(rootCmd.Long).To(Equal("Custom long")) + }) + + It("should apply custom version", func() { + rootCmd := cli.BuildRootCommand(ctx, cli.WithVersion("1.0.0-custom")) + + Expect(rootCmd.Version).To(Equal("1.0.0-custom")) + }) + + It("should apply multiple options", func() { + rootCmd := cli.BuildRootCommand(ctx, + cli.WithUse("mycli"), + cli.WithShort("Custom short"), + cli.WithVersion("2.0.0"), + ) + + Expect(rootCmd.Use).To(Equal("mycli")) + Expect(rootCmd.Short).To(Equal("Custom short")) + Expect(rootCmd.Version).To(Equal("2.0.0")) + }) + + It("should apply custom PersistentPreRun hook", func() { + hookCalled := false + rootCmd := cli.BuildRootCommand(ctx, + cli.WithPersistentPreRun(func(cmd *cobra.Command, args []string) { + hookCalled = true + }), + ) + + Expect(rootCmd.PersistentPreRun).NotTo(BeNil()) + rootCmd.PersistentPreRun(rootCmd, []string{}) + Expect(hookCalled).To(BeTrue()) + }) + + It("should apply custom PersistentPostRun hook", func() { + hookCalled := false + rootCmd := cli.BuildRootCommand(ctx, + cli.WithPersistentPostRun(func(cmd *cobra.Command, args []string) { + hookCalled = true + }), + ) + + Expect(rootCmd.PersistentPostRun).NotTo(BeNil()) + rootCmd.PersistentPostRun(rootCmd, []string{}) + Expect(hookCalled).To(BeTrue()) + }) +}) + +var _ = Describe("RegisterAllCommands", func() { + var ctx *context.Context + var rootCmd *cobra.Command + + BeforeEach(func() { + bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) + ctx = context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) + rootCmd = cli.BuildRootCommand(ctx) + }) + + AfterEach(func() { + if ctx != nil { + ctx.Finalize() + } + }) + + It("should register all commands", func() { + cli.RegisterAllCommands(ctx, rootCmd) + + commands := rootCmd.Commands() + Expect(len(commands)).To(BeNumerically(">", 0)) + + // Check for key commands + commandNames := make(map[string]bool) + for _, cmd := range commands { + commandNames[cmd.Name()] = true + } + + Expect(commandNames).To(HaveKey("exec")) + Expect(commandNames).To(HaveKey("workspace")) + Expect(commandNames).To(HaveKey("config")) + }) +}) + +var _ = Describe("Individual Command Builders", func() { + var ctx *context.Context + + BeforeEach(func() { + bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) + ctx = context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) + }) + + AfterEach(func() { + if ctx != nil { + ctx.Finalize() + } + }) + + It("should build exec command", func() { + cmd := cli.BuildExecCommand(ctx) + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Name()).To(Equal("exec")) + }) + + It("should build browse command", func() { + cmd := cli.BuildBrowseCommand(ctx) + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Name()).To(Equal("browse")) + }) + + It("should build config command", func() { + cmd := cli.BuildConfigCommand(ctx) + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Name()).To(Equal("config")) + }) + + It("should build secret command", func() { + cmd := cli.BuildSecretCommand(ctx) + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Name()).To(Equal("secret")) + }) + + It("should build vault command", func() { + cmd := cli.BuildVaultCommand(ctx) + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Name()).To(Equal("vault")) + }) + + It("should build cache command", func() { + cmd := cli.BuildCacheCommand(ctx) + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Name()).To(Equal("cache")) + }) + + It("should build workspace command", func() { + cmd := cli.BuildWorkspaceCommand(ctx) + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Name()).To(Equal("workspace")) + }) + + It("should build template command", func() { + cmd := cli.BuildTemplateCommand(ctx) + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Name()).To(Equal("template")) + }) + + It("should build logs command", func() { + cmd := cli.BuildLogsCommand(ctx) + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Name()).To(Equal("logs")) + }) + + It("should build sync command", func() { + cmd := cli.BuildSyncCommand(ctx) + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Name()).To(Equal("sync")) + }) + + It("should build mcp command", func() { + cmd := cli.BuildMCPCommand(ctx) + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Name()).To(Equal("mcp")) + }) +}) diff --git a/pkg/cli/doc.go b/pkg/cli/doc.go new file mode 100644 index 00000000..a6724c9c --- /dev/null +++ b/pkg/cli/doc.go @@ -0,0 +1,65 @@ +// Package cli provides a public API for extending and customizing the Flow CLI. +// +// This package allows external projects to: +// - Build custom CLIs that include Flow's commands +// - Add cross-cutting hooks (PreRun/PostRun) to commands +// - Override existing commands with custom implementations +// - Add new commands alongside Flow's built-in commands +// +// # Basic Usage +// +// Build a custom CLI with all Flow commands: +// +// ctx := context.NewContext(...) +// rootCmd := cli.BuildRootCommand(ctx) +// cli.RegisterAllCommands(ctx, rootCmd) +// if err := cli.Execute(ctx, rootCmd); err != nil { +// log.Fatal(err) +// } +// +// # Customizing the Root Command +// +// Use functional options to customize the root command: +// +// rootCmd := cli.BuildRootCommand(ctx, +// cli.WithVersion("1.0.0-custom"), +// cli.WithShort("My custom Flow CLI"), +// cli.WithPersistentPreRun(myPreRunHook), +// ) +// +// # Adding Hooks +// +// Add hooks to individual commands or recursively to all commands: +// +// // Add to a single command +// cli.AddPreRunHook(cmd, telemetryHook) +// +// // Add to all commands recursively +// cli.ApplyHooksRecursive(rootCmd, preHook, postHook) +// +// # Command Registry Operations +// +// Manipulate the command tree: +// +// // Find a command +// wsCmd := cli.FindCommand(rootCmd, "workspace") +// +// // Replace a command +// cli.ReplaceCommand(rootCmd, "exec", customExecCmd) +// +// // Walk all commands +// cli.WalkCommands(rootCmd, func(cmd *cobra.Command) { +// // Do something with each command +// }) +// +// # Thread Safety +// +// This package is not thread-safe. Command building and modification should be +// done during CLI initialization, not concurrently. +// +// # Versioning +// +// This package follows semantic versioning. The API is considered stable but may +// receive additions in minor version updates. Breaking changes will only occur +// in major version updates. +package cli diff --git a/pkg/cli/examples/basic/main.go b/pkg/cli/examples/basic/main.go new file mode 100644 index 00000000..fceb3544 --- /dev/null +++ b/pkg/cli/examples/basic/main.go @@ -0,0 +1,55 @@ +// Package main demonstrates basic usage of the Flow CLI extension API. +// +// This example shows how to: +// - Create a context +// - Build a root command with custom configuration +// - Register all Flow commands +// - Execute the CLI +package main + +import ( + stdCtx "context" + "fmt" + + "github.com/flowexec/flow/internal/io" + "github.com/flowexec/flow/pkg/cli" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" +) + +func main() { + // Load Flow configuration + cfg, err := filesystem.LoadConfig() + if err != nil { + panic(fmt.Errorf("user config load error: %w", err)) + } + + // Initialize logger + loggerOpts := logger.InitOptions{ + StdOut: io.Stdout, + LogMode: cfg.DefaultLogMode, + Theme: io.Theme(cfg.Theme.String()), + } + logger.Init(loggerOpts) + defer logger.Log().Flush() + + // Create context + bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) + ctx := context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) + defer ctx.Finalize() + + // Build root command with custom version + rootCmd := cli.BuildRootCommand(ctx, + cli.WithVersion("1.0.0-example"), + cli.WithShort("Flow CLI - Basic Extension Example"), + ) + + // Register all Flow commands + cli.RegisterAllCommands(ctx, rootCmd) + + // Execute + if err := cli.Execute(ctx, rootCmd); err != nil { + logger.Log().FatalErr(err) + } +} diff --git a/pkg/cli/examples/hooks/main.go b/pkg/cli/examples/hooks/main.go new file mode 100644 index 00000000..a588b69b --- /dev/null +++ b/pkg/cli/examples/hooks/main.go @@ -0,0 +1,89 @@ +// Package main demonstrates hook injection using the Flow CLI extension API. +// +// This example shows how to: +// - Add telemetry/logging hooks to all commands +// - Use PreRun and PostRun hooks +// - Apply hooks recursively +package main + +import ( + stdCtx "context" + "fmt" + "time" + + "github.com/spf13/cobra" + + "github.com/flowexec/flow/internal/io" + "github.com/flowexec/flow/pkg/cli" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" +) + +// commandTiming stores timing information for commands +var commandTiming = make(map[string]time.Time) + +func main() { + // Load Flow configuration + cfg, err := filesystem.LoadConfig() + if err != nil { + panic(fmt.Errorf("user config load error: %w", err)) + } + + // Initialize logger + loggerOpts := logger.InitOptions{ + StdOut: io.Stdout, + LogMode: cfg.DefaultLogMode, + Theme: io.Theme(cfg.Theme.String()), + } + logger.Init(loggerOpts) + defer logger.Log().Flush() + + // Create context + bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) + ctx := context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) + defer ctx.Finalize() + + // Build root command + rootCmd := cli.BuildRootCommand(ctx, + cli.WithVersion("1.0.0-hooks-example"), + cli.WithShort("Flow CLI - Hook Injection Example"), + ) + + // Register all Flow commands + cli.RegisterAllCommands(ctx, rootCmd) + + // Add telemetry hooks to all commands + cli.ApplyHooksRecursive(rootCmd, + // PreRun: Start timing and log command execution + func(cmd *cobra.Command, args []string) { + commandTiming[cmd.Name()] = time.Now() + logger.Log().Infof("Starting command: %s", cmd.Name()) + if len(args) > 0 { + logger.Log().Debugf("Arguments: %v", args) + } + }, + // PostRun: Log completion and timing + func(cmd *cobra.Command, args []string) { + if startTime, ok := commandTiming[cmd.Name()]; ok { + duration := time.Since(startTime) + logger.Log().Infof("Completed command: %s (took %v)", cmd.Name(), duration) + delete(commandTiming, cmd.Name()) + } + }, + ) + + // Add a persistent hook to the root for global initialization/cleanup + cli.AddPersistentPreRunHook(rootCmd, func(cmd *cobra.Command, args []string) { + logger.Log().Debugf("Global PreRun: Initializing resources") + }) + + cli.AddPersistentPostRunHook(rootCmd, func(cmd *cobra.Command, args []string) { + logger.Log().Debugf("Global PostRun: Cleaning up resources") + }) + + // Execute + if err := cli.Execute(ctx, rootCmd); err != nil { + logger.Log().FatalErr(err) + } +} diff --git a/pkg/cli/examples/override/main.go b/pkg/cli/examples/override/main.go new file mode 100644 index 00000000..d6ab4bee --- /dev/null +++ b/pkg/cli/examples/override/main.go @@ -0,0 +1,106 @@ +// Package main demonstrates command overriding using the Flow CLI extension API. +// +// This example shows how to: +// - Replace existing commands with custom implementations +// - Add new commands to the CLI +// - Customize command behavior +package main + +import ( + stdCtx "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/flowexec/flow/internal/io" + "github.com/flowexec/flow/pkg/cli" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" +) + +func main() { + // Load Flow configuration + cfg, err := filesystem.LoadConfig() + if err != nil { + panic(fmt.Errorf("user config load error: %w", err)) + } + + // Initialize logger + loggerOpts := logger.InitOptions{ + StdOut: io.Stdout, + LogMode: cfg.DefaultLogMode, + Theme: io.Theme(cfg.Theme.String()), + } + logger.Init(loggerOpts) + defer logger.Log().Flush() + + // Create context + bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) + ctx := context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) + defer ctx.Finalize() + + // Build root command + rootCmd := cli.BuildRootCommand(ctx, + cli.WithVersion("1.0.0-override-example"), + cli.WithShort("Flow CLI - Command Override Example"), + ) + + // Register all Flow commands + cli.RegisterAllCommands(ctx, rootCmd) + + // Add a custom "premium" command + premiumCmd := &cobra.Command{ + Use: "premium", + Short: "Premium features", + Long: "Access premium features of the Flow CLI", + Run: func(cmd *cobra.Command, args []string) { + logger.Log().PlainTextInfo("Welcome to Premium Flow CLI!") + logger.Log().PlainTextInfo("This is a custom command added via the extension API.") + }, + } + + // Add subcommands to the premium command + premiumCmd.AddCommand(&cobra.Command{ + Use: "status", + Short: "Check premium status", + Run: func(cmd *cobra.Command, args []string) { + logger.Log().PlainTextInfo("Premium Status: Active") + logger.Log().PlainTextInfo("License: Enterprise") + logger.Log().PlainTextInfo("Expiry: Never") + }, + }) + + rootCmd.AddCommand(premiumCmd) + + // Override the version command with custom behavior + // First, let's add a custom version command that shows additional info + customVersionCmd := &cobra.Command{ + Use: "version", + Short: "Show version information with premium details", + Run: func(cmd *cobra.Command, args []string) { + logger.Log().PlainTextInfo("Flow CLI Version: 1.0.0-override-example") + logger.Log().PlainTextInfo("Edition: Premium") + logger.Log().PlainTextInfo("Build Date: 2025-12-26") + logger.Log().PlainTextInfo("License: Enterprise") + }, + } + + // Since version is built into the root command via --version flag, + // we'll add it as a subcommand instead + rootCmd.AddCommand(customVersionCmd) + + // Walk all commands and add annotations + cli.WalkCommands(rootCmd, func(cmd *cobra.Command) { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) + } + cmd.Annotations["edition"] = "premium" + cmd.Annotations["customized"] = "true" + }) + + // Execute + if err := cli.Execute(ctx, rootCmd); err != nil { + logger.Log().FatalErr(err) + } +} diff --git a/pkg/cli/hooks.go b/pkg/cli/hooks.go new file mode 100644 index 00000000..c4b67388 --- /dev/null +++ b/pkg/cli/hooks.go @@ -0,0 +1,245 @@ +package cli + +import "github.com/spf13/cobra" + +// AddPreRunHook adds a PreRun hook to a command, preserving any existing PreRun hook. +// If the command already has a PreRun hook, the new hook will run before it. +// +// Example: +// +// cli.AddPreRunHook(cmd, func(cmd *cobra.Command, args []string) { +// log.Println("Starting command:", cmd.Name()) +// }) +func AddPreRunHook(cmd *cobra.Command, hook HookFunc) { + if cmd == nil || hook == nil { + return + } + + existingHook := cmd.PreRun + if existingHook == nil { + cmd.PreRun = hook + return + } + + // Chain the hooks: new hook runs first, then existing hook + cmd.PreRun = func(c *cobra.Command, args []string) { + hook(c, args) + existingHook(c, args) + } +} + +// AddPostRunHook adds a PostRun hook to a command, preserving any existing PostRun hook. +// If the command already has a PostRun hook, the new hook will run after it. +// +// Example: +// +// cli.AddPostRunHook(cmd, func(cmd *cobra.Command, args []string) { +// log.Println("Finished command:", cmd.Name()) +// }) +func AddPostRunHook(cmd *cobra.Command, hook HookFunc) { + if cmd == nil || hook == nil { + return + } + + existingHook := cmd.PostRun + if existingHook == nil { + cmd.PostRun = hook + return + } + + // Chain the hooks: existing hook runs first, then new hook + cmd.PostRun = func(c *cobra.Command, args []string) { + existingHook(c, args) + hook(c, args) + } +} + +// AddPersistentPreRunHook adds a PersistentPreRun hook to a command, preserving any existing hook. +// If the command already has a PersistentPreRun hook, the new hook will run before it. +// +// PersistentPreRun hooks run before all commands in the subtree. +// +// Example: +// +// cli.AddPersistentPreRunHook(rootCmd, func(cmd *cobra.Command, args []string) { +// log.Println("Initializing...") +// }) +func AddPersistentPreRunHook(cmd *cobra.Command, hook HookFunc) { + if cmd == nil || hook == nil { + return + } + + existingHook := cmd.PersistentPreRun + if existingHook == nil { + cmd.PersistentPreRun = hook + return + } + + // Chain the hooks: new hook runs first, then existing hook + cmd.PersistentPreRun = func(c *cobra.Command, args []string) { + hook(c, args) + existingHook(c, args) + } +} + +// AddPersistentPostRunHook adds a PersistentPostRun hook to a command, preserving any existing hook. +// If the command already has a PersistentPostRun hook, the new hook will run after it. +// +// PersistentPostRun hooks run after all commands in the subtree. +// +// Example: +// +// cli.AddPersistentPostRunHook(rootCmd, func(cmd *cobra.Command, args []string) { +// log.Println("Cleaning up...") +// }) +func AddPersistentPostRunHook(cmd *cobra.Command, hook HookFunc) { + if cmd == nil || hook == nil { + return + } + + existingHook := cmd.PersistentPostRun + if existingHook == nil { + cmd.PersistentPostRun = hook + return + } + + // Chain the hooks: existing hook runs first, then new hook + cmd.PersistentPostRun = func(c *cobra.Command, args []string) { + existingHook(c, args) + hook(c, args) + } +} + +// ApplyHooksRecursive applies PreRun and PostRun hooks to a command and all its subcommands. +// This is useful for adding cross-cutting concerns like telemetry or logging to all commands. +// +// Pass nil for either preRun or postRun to skip adding that hook type. +// +// Example: +// +// cli.ApplyHooksRecursive(rootCmd, +// func(cmd *cobra.Command, args []string) { +// telemetry.Start(cmd.Name()) +// }, +// func(cmd *cobra.Command, args []string) { +// telemetry.End(cmd.Name()) +// }, +// ) +func ApplyHooksRecursive(cmd *cobra.Command, preRun, postRun HookFunc) { + if cmd == nil { + return + } + + // Add hooks to this command + if preRun != nil { + AddPreRunHook(cmd, preRun) + } + if postRun != nil { + AddPostRunHook(cmd, postRun) + } + + // Recursively apply to all subcommands + for _, subCmd := range cmd.Commands() { + ApplyHooksRecursive(subCmd, preRun, postRun) + } +} + +// ApplyPersistentHooksRecursive applies PersistentPreRun and PersistentPostRun hooks +// to a command and all its subcommands. +// +// Pass nil for either preRun or postRun to skip adding that hook type. +// +// Example: +// +// cli.ApplyPersistentHooksRecursive(rootCmd, +// func(cmd *cobra.Command, args []string) { +// // Initialize resources +// }, +// func(cmd *cobra.Command, args []string) { +// // Clean up resources +// }, +// ) +func ApplyPersistentHooksRecursive(cmd *cobra.Command, preRun, postRun HookFunc) { + if cmd == nil { + return + } + + // Add hooks to this command + if preRun != nil { + AddPersistentPreRunHook(cmd, preRun) + } + if postRun != nil { + AddPersistentPostRunHook(cmd, postRun) + } + + // Recursively apply to all subcommands + for _, subCmd := range cmd.Commands() { + ApplyPersistentHooksRecursive(subCmd, preRun, postRun) + } +} + +// WrapRunFunc wraps a command's Run function with before and after hooks. +// This is useful when you want to wrap the actual command execution logic +// rather than using PreRun/PostRun. +// +// Pass nil for either before or after to skip that hook. +// +// Example: +// +// cli.WrapRunFunc(cmd, +// func(cmd *cobra.Command, args []string) { +// log.Println("Before run") +// }, +// func(cmd *cobra.Command, args []string) { +// log.Println("After run") +// }, +// ) +func WrapRunFunc(cmd *cobra.Command, before, after HookFunc) { + if cmd == nil || cmd.Run == nil { + return + } + + existingRun := cmd.Run + cmd.Run = func(c *cobra.Command, args []string) { + if before != nil { + before(c, args) + } + existingRun(c, args) + if after != nil { + after(c, args) + } + } +} + +// WrapRunEFunc wraps a command's RunE function with before and after hooks. +// This is similar to WrapRunFunc but for commands that return errors. +// +// Pass nil for either before or after to skip that hook. +// +// Example: +// +// cli.WrapRunEFunc(cmd, +// func(cmd *cobra.Command, args []string) { +// log.Println("Before run") +// }, +// func(cmd *cobra.Command, args []string) { +// log.Println("After run") +// }, +// ) +func WrapRunEFunc(cmd *cobra.Command, before, after HookFunc) { + if cmd == nil || cmd.RunE == nil { + return + } + + existingRunE := cmd.RunE + cmd.RunE = func(c *cobra.Command, args []string) error { + if before != nil { + before(c, args) + } + err := existingRunE(c, args) + if after != nil { + after(c, args) + } + return err + } +} diff --git a/pkg/cli/hooks_test.go b/pkg/cli/hooks_test.go new file mode 100644 index 00000000..3e0dacb0 --- /dev/null +++ b/pkg/cli/hooks_test.go @@ -0,0 +1,323 @@ +package cli_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/flowexec/flow/pkg/cli" +) + +var _ = Describe("Hook Injection", func() { + Describe("AddPreRunHook", func() { + It("should add PreRun hook to command without existing hook", func() { + cmd := &cobra.Command{Use: "test"} + hookCalled := false + + cli.AddPreRunHook(cmd, func(c *cobra.Command, args []string) { + hookCalled = true + }) + + Expect(cmd.PreRun).NotTo(BeNil()) + cmd.PreRun(cmd, []string{}) + Expect(hookCalled).To(BeTrue()) + }) + + It("should chain with existing PreRun hook", func() { + cmd := &cobra.Command{Use: "test"} + firstCalled := false + secondCalled := false + callOrder := []int{} + + cmd.PreRun = func(c *cobra.Command, args []string) { + firstCalled = true + callOrder = append(callOrder, 1) + } + + cli.AddPreRunHook(cmd, func(c *cobra.Command, args []string) { + secondCalled = true + callOrder = append(callOrder, 2) + }) + + cmd.PreRun(cmd, []string{}) + Expect(firstCalled).To(BeTrue()) + Expect(secondCalled).To(BeTrue()) + Expect(callOrder).To(Equal([]int{2, 1}), "New hook should run before existing hook") + }) + + It("should handle nil command gracefully", func() { + Expect(func() { + cli.AddPreRunHook(nil, func(c *cobra.Command, args []string) {}) + }).NotTo(Panic()) + }) + + It("should handle nil hook gracefully", func() { + cmd := &cobra.Command{Use: "test"} + Expect(func() { + cli.AddPreRunHook(cmd, nil) + }).NotTo(Panic()) + }) + }) + + Describe("AddPostRunHook", func() { + It("should add PostRun hook to command without existing hook", func() { + cmd := &cobra.Command{Use: "test"} + hookCalled := false + + cli.AddPostRunHook(cmd, func(c *cobra.Command, args []string) { + hookCalled = true + }) + + Expect(cmd.PostRun).NotTo(BeNil()) + cmd.PostRun(cmd, []string{}) + Expect(hookCalled).To(BeTrue()) + }) + + It("should chain with existing PostRun hook", func() { + cmd := &cobra.Command{Use: "test"} + firstCalled := false + secondCalled := false + callOrder := []int{} + + cmd.PostRun = func(c *cobra.Command, args []string) { + firstCalled = true + callOrder = append(callOrder, 1) + } + + cli.AddPostRunHook(cmd, func(c *cobra.Command, args []string) { + secondCalled = true + callOrder = append(callOrder, 2) + }) + + cmd.PostRun(cmd, []string{}) + Expect(firstCalled).To(BeTrue()) + Expect(secondCalled).To(BeTrue()) + Expect(callOrder).To(Equal([]int{1, 2}), "New hook should run after existing hook") + }) + }) + + Describe("AddPersistentPreRunHook", func() { + It("should add PersistentPreRun hook", func() { + cmd := &cobra.Command{Use: "test"} + hookCalled := false + + cli.AddPersistentPreRunHook(cmd, func(c *cobra.Command, args []string) { + hookCalled = true + }) + + Expect(cmd.PersistentPreRun).NotTo(BeNil()) + cmd.PersistentPreRun(cmd, []string{}) + Expect(hookCalled).To(BeTrue()) + }) + + It("should chain with existing PersistentPreRun hook", func() { + cmd := &cobra.Command{Use: "test"} + callOrder := []int{} + + cmd.PersistentPreRun = func(c *cobra.Command, args []string) { + callOrder = append(callOrder, 1) + } + + cli.AddPersistentPreRunHook(cmd, func(c *cobra.Command, args []string) { + callOrder = append(callOrder, 2) + }) + + cmd.PersistentPreRun(cmd, []string{}) + Expect(callOrder).To(Equal([]int{2, 1})) + }) + }) + + Describe("AddPersistentPostRunHook", func() { + It("should add PersistentPostRun hook", func() { + cmd := &cobra.Command{Use: "test"} + hookCalled := false + + cli.AddPersistentPostRunHook(cmd, func(c *cobra.Command, args []string) { + hookCalled = true + }) + + Expect(cmd.PersistentPostRun).NotTo(BeNil()) + cmd.PersistentPostRun(cmd, []string{}) + Expect(hookCalled).To(BeTrue()) + }) + + It("should chain with existing PersistentPostRun hook", func() { + cmd := &cobra.Command{Use: "test"} + callOrder := []int{} + + cmd.PersistentPostRun = func(c *cobra.Command, args []string) { + callOrder = append(callOrder, 1) + } + + cli.AddPersistentPostRunHook(cmd, func(c *cobra.Command, args []string) { + callOrder = append(callOrder, 2) + }) + + cmd.PersistentPostRun(cmd, []string{}) + Expect(callOrder).To(Equal([]int{1, 2})) + }) + }) + + Describe("ApplyHooksRecursive", func() { + It("should apply hooks to root and all subcommands", func() { + rootCmd := &cobra.Command{Use: "root"} + subCmd1 := &cobra.Command{Use: "sub1"} + subCmd2 := &cobra.Command{Use: "sub2"} + rootCmd.AddCommand(subCmd1, subCmd2) + + preRunCalls := []string{} + postRunCalls := []string{} + + cli.ApplyHooksRecursive(rootCmd, + func(c *cobra.Command, args []string) { + preRunCalls = append(preRunCalls, c.Use) + }, + func(c *cobra.Command, args []string) { + postRunCalls = append(postRunCalls, c.Use) + }, + ) + + // Verify hooks were added + Expect(rootCmd.PreRun).NotTo(BeNil()) + Expect(rootCmd.PostRun).NotTo(BeNil()) + Expect(subCmd1.PreRun).NotTo(BeNil()) + Expect(subCmd1.PostRun).NotTo(BeNil()) + Expect(subCmd2.PreRun).NotTo(BeNil()) + Expect(subCmd2.PostRun).NotTo(BeNil()) + + // Execute hooks + rootCmd.PreRun(rootCmd, []string{}) + subCmd1.PreRun(subCmd1, []string{}) + subCmd2.PreRun(subCmd2, []string{}) + + Expect(preRunCalls).To(ContainElements("root", "sub1", "sub2")) + + rootCmd.PostRun(rootCmd, []string{}) + subCmd1.PostRun(subCmd1, []string{}) + subCmd2.PostRun(subCmd2, []string{}) + + Expect(postRunCalls).To(ContainElements("root", "sub1", "sub2")) + }) + + It("should handle nil hooks gracefully", func() { + cmd := &cobra.Command{Use: "test"} + + Expect(func() { + cli.ApplyHooksRecursive(cmd, nil, nil) + }).NotTo(Panic()) + }) + + It("should handle nil command gracefully", func() { + Expect(func() { + cli.ApplyHooksRecursive(nil, func(c *cobra.Command, args []string) {}, nil) + }).NotTo(Panic()) + }) + }) + + Describe("ApplyPersistentHooksRecursive", func() { + It("should apply persistent hooks recursively", func() { + rootCmd := &cobra.Command{Use: "root"} + subCmd := &cobra.Command{Use: "sub"} + rootCmd.AddCommand(subCmd) + + hookCalled := false + + cli.ApplyPersistentHooksRecursive(rootCmd, + func(c *cobra.Command, args []string) { + hookCalled = true + }, + nil, + ) + + Expect(rootCmd.PersistentPreRun).NotTo(BeNil()) + Expect(subCmd.PersistentPreRun).NotTo(BeNil()) + + rootCmd.PersistentPreRun(rootCmd, []string{}) + Expect(hookCalled).To(BeTrue()) + }) + }) + + Describe("WrapRunFunc", func() { + It("should wrap Run function with before and after hooks", func() { + callOrder := []string{} + cmd := &cobra.Command{ + Use: "test", + Run: func(c *cobra.Command, args []string) { + callOrder = append(callOrder, "run") + }, + } + + cli.WrapRunFunc(cmd, + func(c *cobra.Command, args []string) { + callOrder = append(callOrder, "before") + }, + func(c *cobra.Command, args []string) { + callOrder = append(callOrder, "after") + }, + ) + + cmd.Run(cmd, []string{}) + Expect(callOrder).To(Equal([]string{"before", "run", "after"})) + }) + + It("should handle nil before hook", func() { + callOrder := []string{} + cmd := &cobra.Command{ + Use: "test", + Run: func(c *cobra.Command, args []string) { + callOrder = append(callOrder, "run") + }, + } + + cli.WrapRunFunc(cmd, nil, func(c *cobra.Command, args []string) { + callOrder = append(callOrder, "after") + }) + + cmd.Run(cmd, []string{}) + Expect(callOrder).To(Equal([]string{"run", "after"})) + }) + + It("should handle nil after hook", func() { + callOrder := []string{} + cmd := &cobra.Command{ + Use: "test", + Run: func(c *cobra.Command, args []string) { + callOrder = append(callOrder, "run") + }, + } + + cli.WrapRunFunc(cmd, func(c *cobra.Command, args []string) { + callOrder = append(callOrder, "before") + }, nil) + + cmd.Run(cmd, []string{}) + Expect(callOrder).To(Equal([]string{"before", "run"})) + }) + }) + + Describe("WrapRunEFunc", func() { + It("should wrap RunE function with before and after hooks", func() { + callOrder := []string{} + cmd := &cobra.Command{ + Use: "test", + RunE: func(c *cobra.Command, args []string) error { + callOrder = append(callOrder, "run") + return nil + }, + } + + cli.WrapRunEFunc(cmd, + func(c *cobra.Command, args []string) { + callOrder = append(callOrder, "before") + }, + func(c *cobra.Command, args []string) { + callOrder = append(callOrder, "after") + }, + ) + + err := cmd.RunE(cmd, []string{}) + Expect(err).NotTo(HaveOccurred()) + Expect(callOrder).To(Equal([]string{"before", "run", "after"})) + }) + }) +}) diff --git a/pkg/cli/registry.go b/pkg/cli/registry.go new file mode 100644 index 00000000..1e61e16b --- /dev/null +++ b/pkg/cli/registry.go @@ -0,0 +1,297 @@ +package cli + +import ( + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +// WalkCommands traverses the command tree starting from the root and applies +// a function to each command (including the root). +// +// The traversal is depth-first, visiting parent commands before their children. +// +// Example: +// +// cli.WalkCommands(rootCmd, func(cmd *cobra.Command) { +// fmt.Println("Command:", cmd.Name()) +// }) +func WalkCommands(rootCmd *cobra.Command, fn func(*cobra.Command)) { + if rootCmd == nil || fn == nil { + return + } + + // Apply function to current command + fn(rootCmd) + + // Recursively apply to all subcommands + for _, subCmd := range rootCmd.Commands() { + WalkCommands(subCmd, fn) + } +} + +// FindCommand finds a command by name in the command tree. +// It performs a breadth-first search starting from the root. +// +// Returns nil if the command is not found. +// +// Example: +// +// wsCmd := cli.FindCommand(rootCmd, "workspace") +// if wsCmd != nil { +// // Found the workspace command +// } +func FindCommand(rootCmd *cobra.Command, name string) *cobra.Command { + if rootCmd == nil || name == "" { + return nil + } + + // Check if this is the command we're looking for + if rootCmd.Name() == name { + return rootCmd + } + + // Search in immediate children first (breadth-first) + for _, cmd := range rootCmd.Commands() { + if cmd.Name() == name { + return cmd + } + } + + // Search recursively in subcommands + for _, cmd := range rootCmd.Commands() { + if found := FindCommand(cmd, name); found != nil { + return found + } + } + + return nil +} + +// FindCommandPath finds a command by its full path (e.g., "workspace add"). +// The path should be space-separated command names. +// +// Returns nil if the command is not found. +// +// Example: +// +// addCmd := cli.FindCommandPath(rootCmd, "workspace add") +// if addCmd != nil { +// // Found the workspace add command +// } +func FindCommandPath(rootCmd *cobra.Command, path string) *cobra.Command { + if rootCmd == nil || path == "" { + return nil + } + + // Use cobra's built-in Find method + cmd, _, err := rootCmd.Find(splitPath(path)) + if err != nil { + return nil + } + return cmd +} + +// ReplaceCommand replaces a command with the given name with a new command. +// The new command will be added to the same parent as the old command. +// +// Returns an error if the old command is not found or if the replacement fails. +// +// Example: +// +// customExec := &cobra.Command{ +// Use: "exec", +// Run: customExecFunc, +// } +// if err := cli.ReplaceCommand(rootCmd, "exec", customExec); err != nil { +// log.Fatal(err) +// } +func ReplaceCommand(rootCmd *cobra.Command, oldName string, newCmd *cobra.Command) error { + if rootCmd == nil || oldName == "" || newCmd == nil { + return fmt.Errorf("invalid parameters: rootCmd, oldName, and newCmd must not be nil/empty") + } + + // Find the command to replace + oldCmd := FindCommand(rootCmd, oldName) + if oldCmd == nil { + return fmt.Errorf("command %q not found", oldName) + } + + // Get the parent of the old command + parent := oldCmd.Parent() + if parent == nil { + return fmt.Errorf("cannot replace root command") + } + + // Remove the old command + parent.RemoveCommand(oldCmd) + + // Add the new command + parent.AddCommand(newCmd) + + return nil +} + +// RemoveCommand removes a command by name from the command tree. +// +// Returns an error if the command is not found or if it's the root command. +// +// Example: +// +// if err := cli.RemoveCommand(rootCmd, "sync"); err != nil { +// log.Fatal(err) +// } +func RemoveCommand(rootCmd *cobra.Command, name string) error { + if rootCmd == nil || name == "" { + return fmt.Errorf("invalid parameters: rootCmd and name must not be nil/empty") + } + + // Find the command + cmd := FindCommand(rootCmd, name) + if cmd == nil { + return fmt.Errorf("command %q not found", name) + } + + // Get the parent + parent := cmd.Parent() + if parent == nil { + return fmt.Errorf("cannot remove root command") + } + + // Remove the command + parent.RemoveCommand(cmd) + + return nil +} + +// ListCommands returns a list of all command names in the tree. +// The list includes the root command and all subcommands. +// +// Example: +// +// names := cli.ListCommands(rootCmd) +// fmt.Println("Available commands:", names) +func ListCommands(rootCmd *cobra.Command) []string { + var names []string + WalkCommands(rootCmd, func(cmd *cobra.Command) { + names = append(names, cmd.Name()) + }) + return names +} + +// GetSubcommands returns a map of subcommand names to commands for a given command. +// This is a convenience wrapper around cobra's Commands() method. +// +// Example: +// +// wsCmd := cli.FindCommand(rootCmd, "workspace") +// subCmds := cli.GetSubcommands(wsCmd) +// for name, cmd := range subCmds { +// fmt.Printf("Subcommand: %s - %s\n", name, cmd.Short) +// } +func GetSubcommands(cmd *cobra.Command) map[string]*cobra.Command { + if cmd == nil { + return nil + } + + subCmds := make(map[string]*cobra.Command) + for _, subCmd := range cmd.Commands() { + subCmds[subCmd.Name()] = subCmd + } + return subCmds +} + +// CloneCommand creates a shallow copy of a command. +// This is useful when you want to modify a command without affecting the original. +// +// Note: This creates a shallow copy - the Run functions and other function fields +// will reference the same functions as the original. +// +// Example: +// +// origCmd := cli.FindCommand(rootCmd, "exec") +// clonedCmd := cli.CloneCommand(origCmd) +// clonedCmd.Short = "Custom exec command" +func CloneCommand(cmd *cobra.Command) *cobra.Command { + if cmd == nil { + return nil + } + + clone := &cobra.Command{ + Use: cmd.Use, + Aliases: append([]string(nil), cmd.Aliases...), + Short: cmd.Short, + Long: cmd.Long, + Example: cmd.Example, + ValidArgs: append([]string(nil), cmd.ValidArgs...), + Args: cmd.Args, + ArgAliases: append([]string(nil), cmd.ArgAliases...), + BashCompletionFunction: cmd.BashCompletionFunction, + Deprecated: cmd.Deprecated, + Hidden: cmd.Hidden, + Annotations: copyMap(cmd.Annotations), + Version: cmd.Version, + PersistentPreRun: cmd.PersistentPreRun, + PersistentPreRunE: cmd.PersistentPreRunE, + PreRun: cmd.PreRun, + PreRunE: cmd.PreRunE, + Run: cmd.Run, + RunE: cmd.RunE, + PostRun: cmd.PostRun, + PostRunE: cmd.PostRunE, + PersistentPostRun: cmd.PersistentPostRun, + PersistentPostRunE: cmd.PersistentPostRunE, + SilenceErrors: cmd.SilenceErrors, + SilenceUsage: cmd.SilenceUsage, + DisableFlagParsing: cmd.DisableFlagParsing, + DisableAutoGenTag: cmd.DisableAutoGenTag, + DisableFlagsInUseLine: cmd.DisableFlagsInUseLine, + DisableSuggestions: cmd.DisableSuggestions, + SuggestionsMinimumDistance: cmd.SuggestionsMinimumDistance, + TraverseChildren: cmd.TraverseChildren, + FParseErrWhitelist: cmd.FParseErrWhitelist, + } + + // Copy flags + cmd.Flags().VisitAll(func(f *pflag.Flag) { + clone.Flags().AddFlag(f) + }) + cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { + clone.PersistentFlags().AddFlag(f) + }) + + return clone +} + +// Helper function to split a command path into individual command names +func splitPath(path string) []string { + var parts []string + current := "" + for _, ch := range path { + if ch == ' ' { + if current != "" { + parts = append(parts, current) + current = "" + } + } else { + current += string(ch) + } + } + if current != "" { + parts = append(parts, current) + } + return parts +} + +// Helper function to copy a map +func copyMap(m map[string]string) map[string]string { + if m == nil { + return nil + } + copy := make(map[string]string, len(m)) + for k, v := range m { + copy[k] = v + } + return copy +} diff --git a/pkg/cli/registry_test.go b/pkg/cli/registry_test.go new file mode 100644 index 00000000..c2fe6ffe --- /dev/null +++ b/pkg/cli/registry_test.go @@ -0,0 +1,293 @@ +package cli_test + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + "github.com/flowexec/flow/pkg/cli" +) + +var _ = Describe("Command Registry", func() { + Describe("WalkCommands", func() { + It("should traverse all commands", func() { + rootCmd := &cobra.Command{Use: "root"} + sub1 := &cobra.Command{Use: "sub1"} + sub2 := &cobra.Command{Use: "sub2"} + subsub := &cobra.Command{Use: "subsub"} + sub1.AddCommand(subsub) + rootCmd.AddCommand(sub1, sub2) + + visited := []string{} + cli.WalkCommands(rootCmd, func(cmd *cobra.Command) { + visited = append(visited, cmd.Use) + }) + + Expect(visited).To(Equal([]string{"root", "sub1", "subsub", "sub2"})) + }) + + It("should handle nil command gracefully", func() { + Expect(func() { + cli.WalkCommands(nil, func(cmd *cobra.Command) {}) + }).NotTo(Panic()) + }) + + It("should handle nil function gracefully", func() { + cmd := &cobra.Command{Use: "test"} + Expect(func() { + cli.WalkCommands(cmd, nil) + }).NotTo(Panic()) + }) + }) + + Describe("FindCommand", func() { + var rootCmd *cobra.Command + + BeforeEach(func() { + rootCmd = &cobra.Command{Use: "root"} + sub1 := &cobra.Command{Use: "sub1"} + sub2 := &cobra.Command{Use: "sub2"} + subsub := &cobra.Command{Use: "subsub"} + sub1.AddCommand(subsub) + rootCmd.AddCommand(sub1, sub2) + }) + + It("should find root command", func() { + cmd := cli.FindCommand(rootCmd, "root") + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Use).To(Equal("root")) + }) + + It("should find immediate child", func() { + cmd := cli.FindCommand(rootCmd, "sub1") + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Use).To(Equal("sub1")) + }) + + It("should find nested command", func() { + cmd := cli.FindCommand(rootCmd, "subsub") + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Use).To(Equal("subsub")) + }) + + It("should return nil for non-existent command", func() { + cmd := cli.FindCommand(rootCmd, "nonexistent") + Expect(cmd).To(BeNil()) + }) + + It("should handle nil root command", func() { + cmd := cli.FindCommand(nil, "test") + Expect(cmd).To(BeNil()) + }) + + It("should handle empty name", func() { + cmd := cli.FindCommand(rootCmd, "") + Expect(cmd).To(BeNil()) + }) + }) + + Describe("FindCommandPath", func() { + var rootCmd *cobra.Command + + BeforeEach(func() { + rootCmd = &cobra.Command{Use: "root"} + sub1 := &cobra.Command{Use: "sub1"} + subsub := &cobra.Command{Use: "subsub"} + sub1.AddCommand(subsub) + rootCmd.AddCommand(sub1) + }) + + It("should find command by single name", func() { + cmd := cli.FindCommandPath(rootCmd, "sub1") + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Use).To(Equal("sub1")) + }) + + It("should find command by path", func() { + cmd := cli.FindCommandPath(rootCmd, "sub1 subsub") + Expect(cmd).NotTo(BeNil()) + Expect(cmd.Use).To(Equal("subsub")) + }) + + It("should return nil for invalid path", func() { + cmd := cli.FindCommandPath(rootCmd, "nonexistent path") + Expect(cmd).To(BeNil()) + }) + }) + + Describe("ReplaceCommand", func() { + var rootCmd *cobra.Command + + BeforeEach(func() { + rootCmd = &cobra.Command{Use: "root"} + oldCmd := &cobra.Command{Use: "old"} + rootCmd.AddCommand(oldCmd) + }) + + It("should replace existing command", func() { + newCmd := &cobra.Command{ + Use: "old", + Short: "New command", + } + + err := cli.ReplaceCommand(rootCmd, "old", newCmd) + Expect(err).NotTo(HaveOccurred()) + + found := cli.FindCommand(rootCmd, "old") + Expect(found).NotTo(BeNil()) + Expect(found.Short).To(Equal("New command")) + }) + + It("should return error for non-existent command", func() { + newCmd := &cobra.Command{Use: "new"} + err := cli.ReplaceCommand(rootCmd, "nonexistent", newCmd) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("not found")) + }) + + It("should return error for nil root", func() { + newCmd := &cobra.Command{Use: "new"} + err := cli.ReplaceCommand(nil, "old", newCmd) + Expect(err).To(HaveOccurred()) + }) + + It("should return error for nil new command", func() { + err := cli.ReplaceCommand(rootCmd, "old", nil) + Expect(err).To(HaveOccurred()) + }) + + It("should return error when trying to replace root", func() { + newCmd := &cobra.Command{Use: "root"} + err := cli.ReplaceCommand(rootCmd, "root", newCmd) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cannot replace root")) + }) + }) + + Describe("RemoveCommand", func() { + var rootCmd *cobra.Command + + BeforeEach(func() { + rootCmd = &cobra.Command{Use: "root"} + cmd1 := &cobra.Command{Use: "cmd1"} + cmd2 := &cobra.Command{Use: "cmd2"} + rootCmd.AddCommand(cmd1, cmd2) + }) + + It("should remove existing command", func() { + err := cli.RemoveCommand(rootCmd, "cmd1") + Expect(err).NotTo(HaveOccurred()) + + found := cli.FindCommand(rootCmd, "cmd1") + Expect(found).To(BeNil()) + + // cmd2 should still exist + found = cli.FindCommand(rootCmd, "cmd2") + Expect(found).NotTo(BeNil()) + }) + + It("should return error for non-existent command", func() { + err := cli.RemoveCommand(rootCmd, "nonexistent") + Expect(err).To(HaveOccurred()) + }) + + It("should return error when trying to remove root", func() { + err := cli.RemoveCommand(rootCmd, "root") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cannot remove root")) + }) + }) + + Describe("ListCommands", func() { + It("should list all command names", func() { + rootCmd := &cobra.Command{Use: "root"} + sub1 := &cobra.Command{Use: "sub1"} + sub2 := &cobra.Command{Use: "sub2"} + rootCmd.AddCommand(sub1, sub2) + + names := cli.ListCommands(rootCmd) + Expect(names).To(ConsistOf("root", "sub1", "sub2")) + }) + + It("should handle empty tree", func() { + rootCmd := &cobra.Command{Use: "root"} + names := cli.ListCommands(rootCmd) + Expect(names).To(Equal([]string{"root"})) + }) + }) + + Describe("GetSubcommands", func() { + It("should return map of subcommands", func() { + rootCmd := &cobra.Command{Use: "root"} + sub1 := &cobra.Command{Use: "sub1"} + sub2 := &cobra.Command{Use: "sub2"} + rootCmd.AddCommand(sub1, sub2) + + subCmds := cli.GetSubcommands(rootCmd) + Expect(subCmds).To(HaveLen(2)) + Expect(subCmds).To(HaveKey("sub1")) + Expect(subCmds).To(HaveKey("sub2")) + Expect(subCmds["sub1"]).To(Equal(sub1)) + Expect(subCmds["sub2"]).To(Equal(sub2)) + }) + + It("should return empty map for command with no subcommands", func() { + cmd := &cobra.Command{Use: "test"} + subCmds := cli.GetSubcommands(cmd) + Expect(subCmds).To(BeEmpty()) + }) + + It("should return nil for nil command", func() { + subCmds := cli.GetSubcommands(nil) + Expect(subCmds).To(BeNil()) + }) + }) + + Describe("CloneCommand", func() { + It("should create a shallow copy of command", func() { + original := &cobra.Command{ + Use: "test", + Short: "Test command", + Long: "Long description", + Aliases: []string{"t", "tst"}, + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("Running") + }, + } + + clone := cli.CloneCommand(original) + Expect(clone).NotTo(BeNil()) + Expect(clone.Use).To(Equal(original.Use)) + Expect(clone.Short).To(Equal(original.Short)) + Expect(clone.Long).To(Equal(original.Long)) + Expect(clone.Aliases).To(Equal(original.Aliases)) + Expect(clone.Run).NotTo(BeNil()) + }) + + It("should handle nil command", func() { + clone := cli.CloneCommand(nil) + Expect(clone).To(BeNil()) + }) + + It("should copy annotations", func() { + original := &cobra.Command{ + Use: "test", + Annotations: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + } + + clone := cli.CloneCommand(original) + Expect(clone.Annotations).To(HaveLen(2)) + Expect(clone.Annotations["key1"]).To(Equal("value1")) + Expect(clone.Annotations["key2"]).To(Equal("value2")) + + // Modify clone's annotations shouldn't affect original + clone.Annotations["key3"] = "value3" + Expect(original.Annotations).NotTo(HaveKey("key3")) + }) + }) +}) diff --git a/pkg/cli/types.go b/pkg/cli/types.go new file mode 100644 index 00000000..d62ad38e --- /dev/null +++ b/pkg/cli/types.go @@ -0,0 +1,79 @@ +package cli + +import "github.com/spf13/cobra" + +// HookFunc is a function that can be used as a PreRun or PostRun hook for a command. +// It receives the command being executed and its arguments. +type HookFunc func(cmd *cobra.Command, args []string) + +// RootConfig holds configuration options for building the root command. +type RootConfig struct { + // Use is the one-line usage message (defaults to "flow") + Use string + + // Short is the short description shown in help (defaults to Flow's standard description) + Short string + + // Long is the long description shown in help (defaults to Flow's standard description) + Long string + + // Version is the version string (defaults to Flow's current version) + Version string + + // PersistentPreRun is a hook that runs before all commands + PersistentPreRun HookFunc + + // PersistentPostRun is a hook that runs after all commands + PersistentPostRun HookFunc +} + +// RootOption is a functional option for configuring the root command. +type RootOption func(*RootConfig) + +// WithUse sets the Use field for the root command. +func WithUse(use string) RootOption { + return func(c *RootConfig) { + c.Use = use + } +} + +// WithShort sets the Short description for the root command. +func WithShort(short string) RootOption { + return func(c *RootConfig) { + c.Short = short + } +} + +// WithLong sets the Long description for the root command. +func WithLong(long string) RootOption { + return func(c *RootConfig) { + c.Long = long + } +} + +// WithVersion sets the Version string for the root command. +func WithVersion(version string) RootOption { + return func(c *RootConfig) { + c.Version = version + } +} + +// WithPersistentPreRun sets a PersistentPreRun hook for the root command. +// This hook will run before all commands in the tree. +// Note: This will replace any existing PersistentPreRun hook from the default +// root command. Use AddPersistentPreRunHook if you want to chain hooks. +func WithPersistentPreRun(hook HookFunc) RootOption { + return func(c *RootConfig) { + c.PersistentPreRun = hook + } +} + +// WithPersistentPostRun sets a PersistentPostRun hook for the root command. +// This hook will run after all commands in the tree. +// Note: This will replace any existing PersistentPostRun hook from the default +// root command. Use AddPersistentPostRunHook if you want to chain hooks. +func WithPersistentPostRun(hook HookFunc) RootOption { + return func(c *RootConfig) { + c.PersistentPostRun = hook + } +} diff --git a/internal/context/context.go b/pkg/context/context.go similarity index 98% rename from internal/context/context.go rename to pkg/context/context.go index 6d499ca9..e36ed139 100644 --- a/internal/context/context.go +++ b/pkg/context/context.go @@ -11,10 +11,10 @@ import ( "github.com/flowexec/tuikit/themes" "github.com/pkg/errors" - "github.com/flowexec/flow/internal/cache" - "github.com/flowexec/flow/internal/filesystem" flowIO "github.com/flowexec/flow/internal/io" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/cache" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/config" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" diff --git a/internal/context/context_test.go b/pkg/context/context_test.go similarity index 100% rename from internal/context/context_test.go rename to pkg/context/context_test.go diff --git a/internal/errors/errors.go b/pkg/errors/errors.go similarity index 100% rename from internal/errors/errors.go rename to pkg/errors/errors.go diff --git a/internal/filesystem/cache.go b/pkg/filesystem/cache.go similarity index 100% rename from internal/filesystem/cache.go rename to pkg/filesystem/cache.go diff --git a/internal/filesystem/cache_test.go b/pkg/filesystem/cache_test.go similarity index 96% rename from internal/filesystem/cache_test.go rename to pkg/filesystem/cache_test.go index 54fcd8e9..339d6a31 100644 --- a/internal/filesystem/cache_test.go +++ b/pkg/filesystem/cache_test.go @@ -4,10 +4,9 @@ import ( "os" "path/filepath" + "github.com/flowexec/flow/pkg/filesystem" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - - "github.com/flowexec/flow/internal/filesystem" ) var _ = Describe("Cache", func() { diff --git a/internal/filesystem/config.go b/pkg/filesystem/config.go similarity index 100% rename from internal/filesystem/config.go rename to pkg/filesystem/config.go diff --git a/internal/filesystem/config_test.go b/pkg/filesystem/config_test.go similarity index 97% rename from internal/filesystem/config_test.go rename to pkg/filesystem/config_test.go index 574f9e09..c77becf7 100644 --- a/internal/filesystem/config_test.go +++ b/pkg/filesystem/config_test.go @@ -7,7 +7,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/flowexec/flow/internal/filesystem" + "github.com/flowexec/flow/pkg/filesystem" "github.com/flowexec/flow/types/config" ) diff --git a/internal/filesystem/executables.go b/pkg/filesystem/executables.go similarity index 99% rename from internal/filesystem/executables.go rename to pkg/filesystem/executables.go index 5cd869b6..77a8961a 100644 --- a/internal/filesystem/executables.go +++ b/pkg/filesystem/executables.go @@ -10,7 +10,7 @@ import ( "github.com/pkg/errors" "gopkg.in/yaml.v3" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" ) diff --git a/internal/filesystem/executables_test.go b/pkg/filesystem/executables_test.go similarity index 98% rename from internal/filesystem/executables_test.go rename to pkg/filesystem/executables_test.go index 18717978..7be3ee6f 100644 --- a/internal/filesystem/executables_test.go +++ b/pkg/filesystem/executables_test.go @@ -9,8 +9,8 @@ import ( . "github.com/onsi/gomega" "go.uber.org/mock/gomock" - "github.com/flowexec/flow/internal/filesystem" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" ) diff --git a/internal/filesystem/filesystem_test.go b/pkg/filesystem/filesystem_test.go similarity index 100% rename from internal/filesystem/filesystem_test.go rename to pkg/filesystem/filesystem_test.go diff --git a/internal/filesystem/helpers.go b/pkg/filesystem/helpers.go similarity index 100% rename from internal/filesystem/helpers.go rename to pkg/filesystem/helpers.go diff --git a/internal/filesystem/logs.go b/pkg/filesystem/logs.go similarity index 100% rename from internal/filesystem/logs.go rename to pkg/filesystem/logs.go diff --git a/internal/filesystem/logs_test.go b/pkg/filesystem/logs_test.go similarity index 92% rename from internal/filesystem/logs_test.go rename to pkg/filesystem/logs_test.go index e25941f6..942aa29f 100644 --- a/internal/filesystem/logs_test.go +++ b/pkg/filesystem/logs_test.go @@ -6,7 +6,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/flowexec/flow/internal/filesystem" + "github.com/flowexec/flow/pkg/filesystem" ) var _ = Describe("Logs", func() { diff --git a/internal/filesystem/templates.go b/pkg/filesystem/templates.go similarity index 100% rename from internal/filesystem/templates.go rename to pkg/filesystem/templates.go diff --git a/internal/filesystem/templates_test.go b/pkg/filesystem/templates_test.go similarity index 98% rename from internal/filesystem/templates_test.go rename to pkg/filesystem/templates_test.go index 7e65b2b0..1ab17ac9 100644 --- a/internal/filesystem/templates_test.go +++ b/pkg/filesystem/templates_test.go @@ -10,7 +10,7 @@ import ( "github.com/pkg/errors" "gopkg.in/yaml.v3" - "github.com/flowexec/flow/internal/filesystem" + "github.com/flowexec/flow/pkg/filesystem" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" ) diff --git a/internal/filesystem/workspace.go b/pkg/filesystem/workspace.go similarity index 100% rename from internal/filesystem/workspace.go rename to pkg/filesystem/workspace.go diff --git a/internal/filesystem/workspace_test.go b/pkg/filesystem/workspace_test.go similarity index 96% rename from internal/filesystem/workspace_test.go rename to pkg/filesystem/workspace_test.go index 55287e9d..fed8558f 100644 --- a/internal/filesystem/workspace_test.go +++ b/pkg/filesystem/workspace_test.go @@ -7,7 +7,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/flowexec/flow/internal/filesystem" + "github.com/flowexec/flow/pkg/filesystem" "github.com/flowexec/flow/types/workspace" ) diff --git a/internal/logger/logger.go b/pkg/logger/logger.go similarity index 100% rename from internal/logger/logger.go rename to pkg/logger/logger.go diff --git a/internal/logger/logger_test.go b/pkg/logger/logger_test.go similarity index 92% rename from internal/logger/logger_test.go rename to pkg/logger/logger_test.go index 540bfe08..bca7d021 100644 --- a/internal/logger/logger_test.go +++ b/pkg/logger/logger_test.go @@ -8,7 +8,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/flowexec/flow/internal/logger" + "github.com/flowexec/flow/pkg/logger" ) func TestLogger(t *testing.T) { diff --git a/tests/utils/context.go b/tests/utils/context.go index 624ae7ab..0cbf4e82 100644 --- a/tests/utils/context.go +++ b/tests/utils/context.go @@ -14,14 +14,14 @@ import ( "go.uber.org/mock/gomock" "gopkg.in/yaml.v3" - "github.com/flowexec/flow/internal/cache" - cacheMocks "github.com/flowexec/flow/internal/cache/mocks" - "github.com/flowexec/flow/internal/context" - "github.com/flowexec/flow/internal/filesystem" "github.com/flowexec/flow/internal/io" - "github.com/flowexec/flow/internal/logger" "github.com/flowexec/flow/internal/runner/mocks" "github.com/flowexec/flow/internal/services/store" + "github.com/flowexec/flow/pkg/cache" + cacheMocks "github.com/flowexec/flow/pkg/cache/mocks" + "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/filesystem" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/tests/utils/builder" "github.com/flowexec/flow/types/config" "github.com/flowexec/flow/types/workspace" diff --git a/tests/utils/runner.go b/tests/utils/runner.go index 34783ea9..a39292af 100644 --- a/tests/utils/runner.go +++ b/tests/utils/runner.go @@ -6,7 +6,7 @@ import ( "os/exec" "github.com/flowexec/flow/cmd" - "github.com/flowexec/flow/internal/context" + "github.com/flowexec/flow/pkg/context" ) type CommandRunner struct{} diff --git a/tools/docsgen/main.go b/tools/docsgen/main.go index 64d15327..287d5e0f 100644 --- a/tools/docsgen/main.go +++ b/tools/docsgen/main.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/cobra/doc" "github.com/flowexec/flow/cmd" - "github.com/flowexec/flow/internal/context" + "github.com/flowexec/flow/pkg/context" ) const ( diff --git a/types/executable/executable.go b/types/executable/executable.go index a4dcf589..f1c7e756 100644 --- a/types/executable/executable.go +++ b/types/executable/executable.go @@ -12,8 +12,8 @@ import ( "github.com/flowexec/tuikit/types" "gopkg.in/yaml.v3" - "github.com/flowexec/flow/internal/errors" "github.com/flowexec/flow/internal/utils" + "github.com/flowexec/flow/pkg/errors" "github.com/flowexec/flow/types/common" ) From 8503703e396413323127f6833cbb257f60528c68 Mon Sep 17 00:00:00 2001 From: Jahvon Dockery Date: Fri, 26 Dec 2025 15:49:42 -0500 Subject: [PATCH 2/2] cleanup --- Dockerfile | 17 + cmd/internal/browse.go | 6 +- cmd/internal/cache.go | 3 +- cmd/internal/config.go | 3 +- cmd/internal/exec.go | 6 +- cmd/internal/helpers.go | 2 +- cmd/internal/secret.go | 5 +- cmd/internal/vault.go | 3 +- cmd/internal/workspace.go | 3 +- cmd/root.go | 10 +- internal/templates/form.go | 4 +- main.go | 6 +- pkg/cache/errors.go | 37 -- pkg/cache/executables_cache.go | 7 +- pkg/cache/executables_cache_test.go | 6 +- pkg/cli/README.md | 401 ------------------- pkg/cli/builders.go | 202 +--------- pkg/cli/builders_test.go | 109 +---- pkg/cli/doc.go | 6 - pkg/cli/{examples/basic => example}/main.go | 40 +- pkg/cli/examples/hooks/main.go | 89 ---- pkg/cli/examples/override/main.go | 106 ----- pkg/cli/hooks.go | 162 -------- pkg/cli/hooks_test.go | 163 -------- pkg/cli/registry.go | 134 ------- pkg/cli/registry_test.go | 48 --- pkg/cli/types.go | 26 -- pkg/context/context.go | 3 +- pkg/errors/errors.go | 25 +- internal/io/styles.go => pkg/logger/theme.go | 2 +- tests/browse_cmds_e2e_test.go | 6 +- tests/utils/context.go | 5 +- tests/utils/runner.go | 4 +- tools/docsgen/main.go | 6 +- types/executable/executable.go | 2 +- 35 files changed, 140 insertions(+), 1517 deletions(-) create mode 100644 Dockerfile delete mode 100644 pkg/cache/errors.go delete mode 100644 pkg/cli/README.md rename pkg/cli/{examples/basic => example}/main.go (50%) delete mode 100644 pkg/cli/examples/hooks/main.go delete mode 100644 pkg/cli/examples/override/main.go rename internal/io/styles.go => pkg/logger/theme.go (93%) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..04c53ba0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM golang:1.25.4-bookworm + +ENV DISABLE_FLOW_INTERACTIVE="true" + +# TODO: replace with examples repo +ENV WORKSPACE="flow" +ENV REPO="https://github.com/flowexec/flow.git" +ENV BRANCH="" + +WORKDIR /workspaces +COPY flow /usr/bin/flow + +RUN if [ -z "$BRANCH" ]; then git clone $REPO .; else git clone -b $BRANCH $REPO .; fi +RUN flow workspace create $WORKSPACE . --set + +ENTRYPOINT ["flow"] +CMD ["--version"] \ No newline at end of file diff --git a/cmd/internal/browse.go b/cmd/internal/browse.go index d6141e3f..27ebc982 100644 --- a/cmd/internal/browse.go +++ b/cmd/internal/browse.go @@ -10,8 +10,8 @@ import ( "github.com/flowexec/flow/internal/io" execIO "github.com/flowexec/flow/internal/io/executable" "github.com/flowexec/flow/internal/io/library" - "github.com/flowexec/flow/pkg/cache" "github.com/flowexec/flow/pkg/context" + flowErrors "github.com/flowexec/flow/pkg/errors" "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/common" "github.com/flowexec/flow/types/executable" @@ -137,7 +137,7 @@ func executableLibrary(ctx *context.Context, cmd *cobra.Command, _ []string) { Substring: subStr, Visibility: visibilityFilter, }, - io.Theme(ctx.Config.Theme.String()), + logger.Theme(ctx.Config.Theme.String()), runFunc, ) SetView(ctx, cmd, libraryModel) @@ -218,7 +218,7 @@ func viewExecutable(ctx *context.Context, cmd *cobra.Command, args []string) { ref := executable.NewRef(execID, verb) exec, err := ctx.ExecutableCache.GetExecutableByRef(ref) - if err != nil && errors.Is(cache.NewExecutableNotFoundError(ref.String()), err) { + if err != nil && errors.Is(flowErrors.NewExecutableNotFoundError(ref.String()), err) { logger.Log().Debugf("Executable %s not found in cache, syncing cache", ref) if err := ctx.ExecutableCache.Update(); err != nil { logger.Log().FatalErr(err) diff --git a/cmd/internal/cache.go b/cmd/internal/cache.go index 602407bf..5d73e2f9 100644 --- a/cmd/internal/cache.go +++ b/cmd/internal/cache.go @@ -8,7 +8,6 @@ import ( "github.com/spf13/cobra" "github.com/flowexec/flow/cmd/internal/flags" - flowIO "github.com/flowexec/flow/internal/io" cacheIO "github.com/flowexec/flow/internal/io/cache" "github.com/flowexec/flow/internal/services/store" "github.com/flowexec/flow/pkg/context" @@ -55,7 +54,7 @@ func cacheSetFunc(ctx *context.Context, cmd *cobra.Command, args []string) { switch { case len(args) == 1: form, err := views.NewForm( - flowIO.Theme(ctx.Config.Theme.String()), + logger.Theme(ctx.Config.Theme.String()), ctx.StdIn(), ctx.StdOut(), &views.FormField{ diff --git a/cmd/internal/config.go b/cmd/internal/config.go index 1a0f345a..bef43a31 100644 --- a/cmd/internal/config.go +++ b/cmd/internal/config.go @@ -12,7 +12,6 @@ import ( "github.com/spf13/cobra" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/io" configIO "github.com/flowexec/flow/internal/io/config" "github.com/flowexec/flow/pkg/context" "github.com/flowexec/flow/pkg/filesystem" @@ -44,7 +43,7 @@ func registerConfigResetCmd(ctx *context.Context, configCmd *cobra.Command) { func resetConfigFunc(ctx *context.Context, _ *cobra.Command, _ []string) { form, err := views.NewForm( - io.Theme(ctx.Config.Theme.String()), + logger.Theme(ctx.Config.Theme.String()), ctx.StdIn(), ctx.StdOut(), &views.FormField{ diff --git a/cmd/internal/exec.go b/cmd/internal/exec.go index 641b10e7..383bdbd3 100644 --- a/cmd/internal/exec.go +++ b/cmd/internal/exec.go @@ -25,8 +25,8 @@ import ( "github.com/flowexec/flow/internal/runner/serial" "github.com/flowexec/flow/internal/services/store" "github.com/flowexec/flow/internal/utils/env" - "github.com/flowexec/flow/pkg/cache" "github.com/flowexec/flow/pkg/context" + flowErrors "github.com/flowexec/flow/pkg/errors" "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" "github.com/flowexec/flow/types/workspace" @@ -109,7 +109,7 @@ func execFunc(ctx *context.Context, cmd *cobra.Command, verb executable.Verb, ar } e, err := ctx.ExecutableCache.GetExecutableByRef(ref) - if err != nil && errors.Is(cache.NewExecutableNotFoundError(ref.String()), err) { + if err != nil && errors.Is(flowErrors.NewExecutableNotFoundError(ref.String()), err) { logger.Log().Debugf("Executable %s not found in cache, syncing cache", ref) if err := ctx.ExecutableCache.Update(); err != nil { logger.Log().FatalErr(err) @@ -160,7 +160,7 @@ func execFunc(ctx *context.Context, cmd *cobra.Command, verb executable.Verb, ar // add values from the prompt param type to the env map textInputs := pendingFormFields(ctx, e, envMap) if len(textInputs) > 0 { - form, err := views.NewForm(io.Theme(ctx.Config.Theme.String()), ctx.StdIn(), ctx.StdOut(), textInputs...) + form, err := views.NewForm(logger.Theme(ctx.Config.Theme.String()), ctx.StdIn(), ctx.StdOut(), textInputs...) if err != nil { logger.Log().FatalErr(err) } diff --git a/cmd/internal/helpers.go b/cmd/internal/helpers.go index 7b5c3b12..b5763345 100644 --- a/cmd/internal/helpers.go +++ b/cmd/internal/helpers.go @@ -98,7 +98,7 @@ func WaitForTUI(ctx *context.Context, cmd *cobra.Command) { func printContext(ctx *context.Context, cmd *cobra.Command) { if TUIEnabled(ctx, cmd) { - logger.Log().Println(flowIO.Theme(ctx.Config.Theme.String()). + logger.Log().Println(logger.Theme(ctx.Config.Theme.String()). RenderHeader(context.AppName, context.HeaderCtxKey, ctx.String(), 0)) } } diff --git a/cmd/internal/secret.go b/cmd/internal/secret.go index 8e4f8e60..974e8d90 100644 --- a/cmd/internal/secret.go +++ b/cmd/internal/secret.go @@ -12,7 +12,6 @@ import ( "github.com/spf13/cobra" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/io" "github.com/flowexec/flow/internal/io/secret" "github.com/flowexec/flow/internal/utils" envUtils "github.com/flowexec/flow/internal/utils/env" @@ -50,7 +49,7 @@ func removeSecretFunc(ctx *context.Context, _ *cobra.Command, args []string) { reference := args[0] form, err := views.NewForm( - io.Theme(ctx.Config.Theme.String()), + logger.Theme(ctx.Config.Theme.String()), ctx.StdIn(), ctx.StdOut(), &views.FormField{ @@ -116,7 +115,7 @@ func setSecretFunc(ctx *context.Context, cmd *cobra.Command, args []string) { value = string(data) case len(args) == 1: form, err := views.NewForm( - io.Theme(ctx.Config.Theme.String()), + logger.Theme(ctx.Config.Theme.String()), ctx.StdIn(), ctx.StdOut(), &views.FormField{ diff --git a/cmd/internal/vault.go b/cmd/internal/vault.go index ae172af9..9221590a 100644 --- a/cmd/internal/vault.go +++ b/cmd/internal/vault.go @@ -11,7 +11,6 @@ import ( "golang.org/x/exp/maps" "github.com/flowexec/flow/cmd/internal/flags" - flowIO "github.com/flowexec/flow/internal/io" vaultIO "github.com/flowexec/flow/internal/io/vault" "github.com/flowexec/flow/internal/utils" "github.com/flowexec/flow/internal/vault" @@ -228,7 +227,7 @@ func removeVaultFunc(ctx *context.Context, _ *cobra.Command, args []string) { } form, err := views.NewForm( - flowIO.Theme(ctx.Config.Theme.String()), + logger.Theme(ctx.Config.Theme.String()), ctx.StdIn(), ctx.StdOut(), &views.FormField{ diff --git a/cmd/internal/workspace.go b/cmd/internal/workspace.go index 26b9ef11..74efdb6f 100644 --- a/cmd/internal/workspace.go +++ b/cmd/internal/workspace.go @@ -13,7 +13,6 @@ import ( "golang.org/x/exp/maps" "github.com/flowexec/flow/cmd/internal/flags" - "github.com/flowexec/flow/internal/io" workspaceIO "github.com/flowexec/flow/internal/io/workspace" "github.com/flowexec/flow/pkg/cache" "github.com/flowexec/flow/pkg/context" @@ -167,7 +166,7 @@ func removeWorkspaceFunc(ctx *context.Context, _ *cobra.Command, args []string) name := args[0] form, err := views.NewForm( - io.Theme(ctx.Config.Theme.String()), + logger.Theme(ctx.Config.Theme.String()), ctx.StdIn(), ctx.StdOut(), &views.FormField{ diff --git a/cmd/root.go b/cmd/root.go index fab1978c..4cd616eb 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -44,6 +44,11 @@ func NewRootCmd(ctx *context.Context) *cobra.Command { } internal.RegisterPersistentFlag(ctx, rootCmd, *flags.LogLevel) internal.RegisterPersistentFlag(ctx, rootCmd, *flags.SyncCacheFlag) + + rootCmd.SetOut(ctx.StdOut()) + rootCmd.SetErr(ctx.StdOut()) + rootCmd.SetIn(ctx.StdIn()) + return rootCmd } @@ -54,11 +59,6 @@ func Execute(ctx *context.Context, rootCmd *cobra.Command) error { panic("root command is not initialized") } - rootCmd.SetOut(ctx.StdOut()) - rootCmd.SetErr(ctx.StdOut()) - rootCmd.SetIn(ctx.StdIn()) - RegisterSubCommands(ctx, rootCmd) - if err := rootCmd.Execute(); err != nil { return fmt.Errorf("failed to execute command: %w", err) } diff --git a/internal/templates/form.go b/internal/templates/form.go index 6f945076..c8887f44 100644 --- a/internal/templates/form.go +++ b/internal/templates/form.go @@ -5,8 +5,8 @@ import ( "github.com/flowexec/tuikit/views" - "github.com/flowexec/flow/internal/io" "github.com/flowexec/flow/pkg/context" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/executable" ) @@ -47,7 +47,7 @@ func showForm(ctx *context.Context, fields executable.FormFields) error { ValidationExpr: f.Validate, }) } - form, err := views.NewForm(io.Theme(ctx.Config.Theme.String()), in, out, ff...) + form, err := views.NewForm(logger.Theme(ctx.Config.Theme.String()), in, out, ff...) if err != nil { return fmt.Errorf("encountered form init error: %w", err) } diff --git a/main.go b/main.go index fbe2cf59..13da032e 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "github.com/flowexec/flow/cmd" "github.com/flowexec/flow/internal/io" + "github.com/flowexec/flow/pkg/cli" "github.com/flowexec/flow/pkg/context" "github.com/flowexec/flow/pkg/filesystem" "github.com/flowexec/flow/pkg/logger" @@ -29,7 +30,7 @@ func main() { loggerOpts := logger.InitOptions{ StdOut: io.Stdout, LogMode: cfg.DefaultLogMode, - Theme: io.Theme(cfg.Theme.String()), + Theme: logger.Theme(cfg.Theme.String()), ArchiveDirectory: archiveDir, } logger.Init(loggerOpts) @@ -49,7 +50,8 @@ func main() { if ctx == nil { panic("failed to initialize context") } - rootCmd := cmd.NewRootCmd(ctx) + rootCmd := cli.BuildRootCommand(ctx) + cli.RegisterAllCommands(ctx, rootCmd) if err := cmd.Execute(ctx, rootCmd); err != nil { logger.Log().FatalErr(err) } diff --git a/pkg/cache/errors.go b/pkg/cache/errors.go deleted file mode 100644 index b3732b5d..00000000 --- a/pkg/cache/errors.go +++ /dev/null @@ -1,37 +0,0 @@ -package cache - -import ( - "fmt" -) - -type ExecutableNotFoundError struct { - Ref string -} - -func (e ExecutableNotFoundError) Error() string { - return fmt.Sprintf("unable to find executable with reference %s", e.Ref) -} - -func (e ExecutableNotFoundError) Unwrap() error { - return fmt.Errorf("executable not found") -} - -func NewExecutableNotFoundError(ref string) ExecutableNotFoundError { - return ExecutableNotFoundError{Ref: ref} -} - -type CacheUpdateError struct { - Err error -} - -func (e CacheUpdateError) Error() string { - return fmt.Sprintf("unable to update cache - %v", e.Err) -} - -func (e CacheUpdateError) Unwrap() error { - return e.Err -} - -func NewCacheUpdateError(err error) CacheUpdateError { - return CacheUpdateError{Err: err} -} diff --git a/pkg/cache/executables_cache.go b/pkg/cache/executables_cache.go index 97d1e5d7..74259fc4 100644 --- a/pkg/cache/executables_cache.go +++ b/pkg/cache/executables_cache.go @@ -7,6 +7,7 @@ import ( "gopkg.in/yaml.v3" "github.com/flowexec/flow/internal/fileparser" + flowErrors "github.com/flowexec/flow/pkg/errors" "github.com/flowexec/flow/pkg/filesystem" "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/types/common" @@ -171,10 +172,10 @@ func (c *ExecutableCacheImpl) GetExecutableByRef(ref executable.Ref) (*executabl primaryRef = aliasedPrimaryRef cfgPath, found = c.Data.ExecutableMap[primaryRef] if !found { - return nil, NewExecutableNotFoundError(ref.String()) + return nil, flowErrors.NewExecutableNotFoundError(ref.String()) } } else { - return nil, NewExecutableNotFoundError(ref.String()) + return nil, flowErrors.NewExecutableNotFoundError(ref.String()) } } else { primaryRef = ref @@ -208,7 +209,7 @@ func (c *ExecutableCacheImpl) GetExecutableByRef(ref executable.Ref) (*executabl if err != nil { return nil, err } else if exec == nil { - return nil, NewExecutableNotFoundError(ref.String()) + return nil, flowErrors.NewExecutableNotFoundError(ref.String()) } c.Data.loadedExecutables[ref.String()] = exec diff --git a/pkg/cache/executables_cache_test.go b/pkg/cache/executables_cache_test.go index a5bfc99b..da5607d2 100644 --- a/pkg/cache/executables_cache_test.go +++ b/pkg/cache/executables_cache_test.go @@ -262,7 +262,7 @@ var _ = Describe("ExecutableCacheImpl", func() { execRef := executable.Ref("exec test/testdata:test-alias") _, err := execCache.GetExecutableByRef(execRef) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("unable to find executable")) + Expect(err.Error()).To(ContainSubstring("executable not found")) // Should still be able to access via primary verb "run" runRef := executable.Ref("run test/testdata:test-alias") @@ -318,7 +318,7 @@ var _ = Describe("ExecutableCacheImpl", func() { executeRef := executable.Ref("execute test/testdata:test-alias") _, err = execCache.GetExecutableByRef(executeRef) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("unable to find executable")) + Expect(err.Error()).To(ContainSubstring("executable not found")) }) }) @@ -340,7 +340,7 @@ var _ = Describe("ExecutableCacheImpl", func() { execRef := executable.Ref("exec test/testdata:test-alias") _, err := execCache.GetExecutableByRef(execRef) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("unable to find executable")) + Expect(err.Error()).To(ContainSubstring("executable not found")) // Should still be able to access via primary verb "run" runRef := executable.Ref("run test/testdata:test-alias") diff --git a/pkg/cli/README.md b/pkg/cli/README.md deleted file mode 100644 index 75d8cfcb..00000000 --- a/pkg/cli/README.md +++ /dev/null @@ -1,401 +0,0 @@ -# Flow CLI Extension API - -The `pkg/cli` package provides a public API for extending and customizing the Flow CLI. This allows external projects to build custom CLIs that include Flow's commands, add cross-cutting functionality, and override existing behavior. - -## Table of Contents - -- [Features](#features) -- [Installation](#installation) -- [Quick Start](#quick-start) -- [API Overview](#api-overview) - - [Command Builders](#command-builders) - - [Hook Injection](#hook-injection) - - [Command Registry](#command-registry) - - [Root Command Customization](#root-command-customization) -- [Examples](#examples) -- [API Reference](#api-reference) -- [Best Practices](#best-practices) - -## Features - -- **Command Builders**: Create Flow commands programmatically -- **Hook Injection**: Add PreRun/PostRun hooks to any command -- **Command Registry**: Manipulate the command tree (find, replace, remove) -- **Root Customization**: Customize the root command with functional options -- **Backward Compatible**: Works alongside standard Flow CLI - -## Installation - -```bash -go get github.com/flowexec/flow -``` - -Then import the package: - -```go -import "github.com/flowexec/flow/pkg/cli" -``` - -## Quick Start - -Here's a minimal example of building a custom Flow CLI: - -```go -package main - -import ( - stdCtx "context" - - "github.com/flowexec/flow/pkg/context" - "github.com/flowexec/flow/pkg/filessystem" - "github.com/flowexec/flow/internal/io" - "github.com/flowexec/flow/pkg/logger" - "github.com/flowexec/flow/pkg/cli" -) - -func main() { - // Setup (same as standard Flow CLI) - cfg, _ := filesystem.LoadConfig() - logger.Init(logger.InitOptions{ - StdOut: io.Stdout, - LogMode: cfg.DefaultLogMode, - Theme: io.Theme(cfg.Theme.String()), - }) - defer logger.Log().Flush() - - // Create context - bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) - ctx := context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) - defer ctx.Finalize() - - // Build custom CLI - rootCmd := cli.BuildRootCommand(ctx, - cli.WithVersion("1.0.0-custom"), - ) - cli.RegisterAllCommands(ctx, rootCmd) - - // Execute - cli.Execute(ctx, rootCmd) -} -``` - -## API Overview - -### Command Builders - -Create Flow commands programmatically: - -```go -// Build root command -rootCmd := cli.BuildRootCommand(ctx, - cli.WithVersion("1.0.0"), - cli.WithShort("My custom CLI"), -) - -// Register all commands at once -cli.RegisterAllCommands(ctx, rootCmd) - -// Or build individual commands -execCmd := cli.BuildExecCommand(ctx) -wsCmd := cli.BuildWorkspaceCommand(ctx) -rootCmd.AddCommand(execCmd, wsCmd) -``` - -Available command builders: -- `BuildRootCommand(ctx, ...opts)` - Root command -- `BuildExecCommand(ctx)` - Exec command -- `BuildBrowseCommand(ctx)` - Browse command -- `BuildConfigCommand(ctx)` - Config command -- `BuildSecretCommand(ctx)` - Secret command -- `BuildVaultCommand(ctx)` - Vault command -- `BuildCacheCommand(ctx)` - Cache command -- `BuildWorkspaceCommand(ctx)` - Workspace command -- `BuildTemplateCommand(ctx)` - Template command -- `BuildLogsCommand(ctx)` - Logs command -- `BuildSyncCommand(ctx)` - Sync command -- `BuildMCPCommand(ctx)` - MCP command - -### Hook Injection - -Add cross-cutting functionality to commands: - -```go -// Add hooks to a single command -cli.AddPreRunHook(cmd, func(cmd *cobra.Command, args []string) { - log.Println("Before command:", cmd.Name()) -}) - -cli.AddPostRunHook(cmd, func(cmd *cobra.Command, args []string) { - log.Println("After command:", cmd.Name()) -}) - -// Add hooks to all commands recursively -cli.ApplyHooksRecursive(rootCmd, - // PreRun hook - func(cmd *cobra.Command, args []string) { - telemetry.Start(cmd.Name()) - }, - // PostRun hook - func(cmd *cobra.Command, args []string) { - telemetry.End(cmd.Name()) - }, -) - -// Add persistent hooks (inherited by subcommands) -cli.AddPersistentPreRunHook(rootCmd, initHook) -cli.AddPersistentPostRunHook(rootCmd, cleanupHook) -``` - -Hook functions: -- `AddPreRunHook(cmd, hook)` - Add PreRun hook -- `AddPostRunHook(cmd, hook)` - Add PostRun hook -- `AddPersistentPreRunHook(cmd, hook)` - Add PersistentPreRun hook -- `AddPersistentPostRunHook(cmd, hook)` - Add PersistentPostRun hook -- `ApplyHooksRecursive(cmd, preRun, postRun)` - Apply to entire tree -- `ApplyPersistentHooksRecursive(cmd, preRun, postRun)` - Apply persistent to tree - -### Command Registry - -Manipulate the command tree: - -```go -// Find a command -wsCmd := cli.FindCommand(rootCmd, "workspace") - -// Find by path -addCmd := cli.FindCommandPath(rootCmd, "workspace add") - -// Replace a command -customExec := &cobra.Command{...} -cli.ReplaceCommand(rootCmd, "exec", customExec) - -// Remove a command -cli.RemoveCommand(rootCmd, "sync") - -// Walk all commands -cli.WalkCommands(rootCmd, func(cmd *cobra.Command) { - fmt.Println("Command:", cmd.Name()) -}) - -// List all command names -names := cli.ListCommands(rootCmd) - -// Get subcommands -subCmds := cli.GetSubcommands(wsCmd) -``` - -Registry functions: -- `WalkCommands(root, fn)` - Traverse command tree -- `FindCommand(root, name)` - Find command by name -- `FindCommandPath(root, path)` - Find by full path -- `ReplaceCommand(root, name, newCmd)` - Replace command -- `RemoveCommand(root, name)` - Remove command -- `ListCommands(root)` - Get all command names -- `GetSubcommands(cmd)` - Get subcommands map -- `CloneCommand(cmd)` - Clone a command - -### Root Command Customization - -Use functional options to customize the root command: - -```go -rootCmd := cli.BuildRootCommand(ctx, - cli.WithUse("mycli"), - cli.WithShort("My custom CLI description"), - cli.WithLong("Detailed description..."), - cli.WithVersion("1.0.0"), - cli.WithPersistentPreRun(func(cmd *cobra.Command, args []string) { - // Global initialization - }), - cli.WithPersistentPostRun(func(cmd *cobra.Command, args []string) { - // Global cleanup - }), -) -``` - -Available options: -- `WithUse(use)` - Set Use field -- `WithShort(short)` - Set Short description -- `WithLong(long)` - Set Long description -- `WithVersion(version)` - Set Version string -- `WithPersistentPreRun(hook)` - Set PersistentPreRun hook -- `WithPersistentPostRun(hook)` - Set PersistentPostRun hook - -## Examples - -See the [examples](./examples) directory for complete working examples: - -- **[basic](./examples/basic/main.go)** - Basic CLI with custom version -- **[hooks](./examples/hooks/main.go)** - Adding telemetry hooks to all commands -- **[override](./examples/override/main.go)** - Overriding commands and adding new ones - -## API Reference - -### Types - -```go -// HookFunc is a function that can be used as a PreRun or PostRun hook -type HookFunc func(cmd *cobra.Command, args []string) - -// RootConfig holds configuration options for building the root command -type RootConfig struct { - Use string - Short string - Long string - Version string - PersistentPreRun HookFunc - PersistentPostRun HookFunc -} - -// RootOption is a functional option for configuring the root command -type RootOption func(*RootConfig) -``` - -### Core Functions - -```go -// Build and execute -BuildRootCommand(ctx *context.Context, opts ...RootOption) *cobra.Command -RegisterAllCommands(ctx *context.Context, rootCmd *cobra.Command) -Execute(ctx *context.Context, rootCmd *cobra.Command) error - -// Command builders -BuildExecCommand(ctx *context.Context) *cobra.Command -BuildBrowseCommand(ctx *context.Context) *cobra.Command -BuildConfigCommand(ctx *context.Context) *cobra.Command -BuildSecretCommand(ctx *context.Context) *cobra.Command -BuildVaultCommand(ctx *context.Context) *cobra.Command -BuildCacheCommand(ctx *context.Context) *cobra.Command -BuildWorkspaceCommand(ctx *context.Context) *cobra.Command -BuildTemplateCommand(ctx *context.Context) *cobra.Command -BuildLogsCommand(ctx *context.Context) *cobra.Command -BuildSyncCommand(ctx *context.Context) *cobra.Command -BuildMCPCommand(ctx *context.Context) *cobra.Command - -// Hook injection -AddPreRunHook(cmd *cobra.Command, hook HookFunc) -AddPostRunHook(cmd *cobra.Command, hook HookFunc) -AddPersistentPreRunHook(cmd *cobra.Command, hook HookFunc) -AddPersistentPostRunHook(cmd *cobra.Command, hook HookFunc) -ApplyHooksRecursive(cmd *cobra.Command, preRun, postRun HookFunc) -ApplyPersistentHooksRecursive(cmd *cobra.Command, preRun, postRun HookFunc) -WrapRunFunc(cmd *cobra.Command, before, after HookFunc) -WrapRunEFunc(cmd *cobra.Command, before, after HookFunc) - -// Command registry -WalkCommands(rootCmd *cobra.Command, fn func(*cobra.Command)) -FindCommand(rootCmd *cobra.Command, name string) *cobra.Command -FindCommandPath(rootCmd *cobra.Command, path string) *cobra.Command -ReplaceCommand(rootCmd *cobra.Command, oldName string, newCmd *cobra.Command) error -RemoveCommand(rootCmd *cobra.Command, name string) error -ListCommands(rootCmd *cobra.Command) []string -GetSubcommands(cmd *cobra.Command) map[string]*cobra.Command -CloneCommand(cmd *cobra.Command) *cobra.Command - -// Root options -WithUse(use string) RootOption -WithShort(short string) RootOption -WithLong(long string) RootOption -WithVersion(version string) RootOption -WithPersistentPreRun(hook HookFunc) RootOption -WithPersistentPostRun(hook HookFunc) RootOption -``` - -## Best Practices - -### 1. Hook Ordering - -Hooks are chained in the order they're added: -- **PreRun**: New hooks run before existing hooks -- **PostRun**: New hooks run after existing hooks - -```go -// This hook runs first -cli.AddPreRunHook(cmd, hook1) -// This hook runs second -cli.AddPreRunHook(cmd, hook2) -``` - -### 2. Context Management - -Always create and finalize the Flow context properly: - -```go -bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) -ctx := context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) -defer ctx.Finalize() // Important! -``` - -### 3. Command Replacement - -When replacing commands, ensure the new command has the same `Use` field: - -```go -// Bad: Use field doesn't match -newCmd := &cobra.Command{Use: "execute", ...} -cli.ReplaceCommand(rootCmd, "exec", newCmd) // Won't work as expected - -// Good: Use field matches -newCmd := &cobra.Command{Use: "exec", ...} -cli.ReplaceCommand(rootCmd, "exec", newCmd) // Works correctly -``` - -### 4. Error Handling - -Always check errors from registry operations: - -```go -if err := cli.ReplaceCommand(rootCmd, "exec", newCmd); err != nil { - log.Fatalf("Failed to replace command: %v", err) -} -``` - -### 5. Hook Safety - -Hooks should be safe to call multiple times and handle nil values: - -```go -// Good: Safe hook implementation -myHook := func(cmd *cobra.Command, args []string) { - if cmd == nil { - return - } - // Do work... -} -``` - -### 6. Building Individual Commands - -When building individual commands instead of using `RegisterAllCommands`, remember that some commands may have dependencies: - -```go -// Build commands with potential dependencies -execCmd := cli.BuildExecCommand(ctx) -cacheCmd := cli.BuildCacheCommand(ctx) // Exec may depend on cache - -rootCmd.AddCommand(execCmd, cacheCmd) -``` - -## Thread Safety - -This package is **not thread-safe**. Command building and modification should be done during CLI initialization, not concurrently. - -## Versioning - -This package follows semantic versioning: -- **Major**: Breaking changes to the public API -- **Minor**: New features, backward compatible -- **Patch**: Bug fixes, backward compatible - -## Contributing - -When contributing to this package: -1. Maintain backward compatibility -2. Add comprehensive godoc comments -3. Include examples for new features -4. Write tests for all public functions -5. Update this README with new features - -## License - -This package is part of the Flow CLI project and follows the same license. diff --git a/pkg/cli/builders.go b/pkg/cli/builders.go index fca53ab9..435bde27 100644 --- a/pkg/cli/builders.go +++ b/pkg/cli/builders.go @@ -12,204 +12,40 @@ import ( // // By default, this creates a root command with Flow's standard configuration. // Use RootOption functions to customize the command. -// -// Example: -// -// rootCmd := cli.BuildRootCommand(ctx, -// cli.WithVersion("1.0.0-custom"), -// cli.WithShort("My custom Flow CLI"), -// ) func BuildRootCommand(ctx *context.Context, opts ...RootOption) *cobra.Command { - // Create the root command using Flow's standard builder rootCmd := cmd.NewRootCmd(ctx) - // Apply any custom configuration - if len(opts) > 0 { - config := &RootConfig{} - for _, opt := range opts { - opt(config) - } + if len(opts) == 0 { + return rootCmd + } + + config := &RootConfig{} + for _, opt := range opts { + opt(config) + } - if config.Use != "" { - rootCmd.Use = config.Use - } - if config.Short != "" { - rootCmd.Short = config.Short - } - if config.Long != "" { - rootCmd.Long = config.Long - } - if config.Version != "" { - rootCmd.Version = config.Version - } - if config.PersistentPreRun != nil { - rootCmd.PersistentPreRun = config.PersistentPreRun - } - if config.PersistentPostRun != nil { - rootCmd.PersistentPostRun = config.PersistentPostRun - } + if config.Use != "" { + rootCmd.Use = config.Use + } + if config.Short != "" { + rootCmd.Short = config.Short + } + if config.Long != "" { + rootCmd.Long = config.Long + } + if config.Version != "" { + rootCmd.Version = config.Version } return rootCmd } // RegisterAllCommands registers all Flow commands to the root command. -// This includes: exec, browse, config, secret, vault, cache, workspace, template, logs, sync, mcp. -// -// This is a convenience function that calls all the individual Register*Command functions. -// -// Example: -// -// rootCmd := cli.BuildRootCommand(ctx) -// cli.RegisterAllCommands(ctx, rootCmd) func RegisterAllCommands(ctx *context.Context, rootCmd *cobra.Command) { cmd.RegisterSubCommands(ctx, rootCmd) } // Execute runs the root command. This is a convenience wrapper around cobra's Execute. -// It configures the command's IO streams and executes it. -// -// Example: -// -// rootCmd := cli.BuildRootCommand(ctx) -// cli.RegisterAllCommands(ctx, rootCmd) -// if err := cli.Execute(ctx, rootCmd); err != nil { -// log.Fatal(err) -// } func Execute(ctx *context.Context, rootCmd *cobra.Command) error { return cmd.Execute(ctx, rootCmd) } - -// BuildExecCommand creates the exec command with all its configuration. -// The exec command is used to execute workflows by their verb and reference. -// -// This command is typically registered automatically via RegisterAllCommands, -// but can be built separately for customization or replacement. -// -// Note: Individual command builders require commands to be registered to a parent -// first, so this creates a temporary parent and returns the registered command. -// -// Example: -// -// execCmd := cli.BuildExecCommand(ctx) -// // Customize execCmd... -// rootCmd.AddCommand(execCmd) -func BuildExecCommand(ctx *context.Context) *cobra.Command { - return buildCommand(ctx, "exec") -} - -// BuildBrowseCommand creates the browse command for browsing executables. -// -// Example: -// -// browseCmd := cli.BuildBrowseCommand(ctx) -// rootCmd.AddCommand(browseCmd) -func BuildBrowseCommand(ctx *context.Context) *cobra.Command { - return buildCommand(ctx, "browse") -} - -// BuildConfigCommand creates the config command for managing Flow configuration. -// -// Example: -// -// configCmd := cli.BuildConfigCommand(ctx) -// rootCmd.AddCommand(configCmd) -func BuildConfigCommand(ctx *context.Context) *cobra.Command { - return buildCommand(ctx, "config") -} - -// BuildSecretCommand creates the secret command for managing secrets. -// -// Example: -// -// secretCmd := cli.BuildSecretCommand(ctx) -// rootCmd.AddCommand(secretCmd) -func BuildSecretCommand(ctx *context.Context) *cobra.Command { - return buildCommand(ctx, "secret") -} - -// BuildVaultCommand creates the vault command for managing the secrets vault. -// -// Example: -// -// vaultCmd := cli.BuildVaultCommand(ctx) -// rootCmd.AddCommand(vaultCmd) -func BuildVaultCommand(ctx *context.Context) *cobra.Command { - return buildCommand(ctx, "vault") -} - -// BuildCacheCommand creates the cache command for managing Flow's cache. -// -// Example: -// -// cacheCmd := cli.BuildCacheCommand(ctx) -// rootCmd.AddCommand(cacheCmd) -func BuildCacheCommand(ctx *context.Context) *cobra.Command { - return buildCommand(ctx, "cache") -} - -// BuildWorkspaceCommand creates the workspace command for managing workspaces. -// -// Example: -// -// wsCmd := cli.BuildWorkspaceCommand(ctx) -// rootCmd.AddCommand(wsCmd) -func BuildWorkspaceCommand(ctx *context.Context) *cobra.Command { - return buildCommand(ctx, "workspace") -} - -// BuildTemplateCommand creates the template command for managing templates. -// -// Example: -// -// templateCmd := cli.BuildTemplateCommand(ctx) -// rootCmd.AddCommand(templateCmd) -func BuildTemplateCommand(ctx *context.Context) *cobra.Command { - return buildCommand(ctx, "template") -} - -// BuildLogsCommand creates the logs command for viewing execution logs. -// -// Example: -// -// logsCmd := cli.BuildLogsCommand(ctx) -// rootCmd.AddCommand(logsCmd) -func BuildLogsCommand(ctx *context.Context) *cobra.Command { - return buildCommand(ctx, "logs") -} - -// BuildSyncCommand creates the sync command for synchronizing caches. -// -// Example: -// -// syncCmd := cli.BuildSyncCommand(ctx) -// rootCmd.AddCommand(syncCmd) -func BuildSyncCommand(ctx *context.Context) *cobra.Command { - return buildCommand(ctx, "sync") -} - -// BuildMCPCommand creates the mcp command for MCP server management. -// -// Example: -// -// mcpCmd := cli.BuildMCPCommand(ctx) -// rootCmd.AddCommand(mcpCmd) -func BuildMCPCommand(ctx *context.Context) *cobra.Command { - return buildCommand(ctx, "mcp") -} - -// buildCommand is a helper that creates a temporary root, registers all commands, -// and returns the requested command. This is necessary because the cmd package -// uses internal registration functions that we can't access directly. -func buildCommand(ctx *context.Context, name string) *cobra.Command { - tempRoot := &cobra.Command{} - cmd.RegisterSubCommands(ctx, tempRoot) - - // Find and return the command with the matching name - for _, c := range tempRoot.Commands() { - if c.Name() == name { - return c - } - } - return nil -} diff --git a/pkg/cli/builders_test.go b/pkg/cli/builders_test.go index 16417484..6a07656a 100644 --- a/pkg/cli/builders_test.go +++ b/pkg/cli/builders_test.go @@ -75,32 +75,6 @@ var _ = Describe("BuildRootCommand", func() { Expect(rootCmd.Short).To(Equal("Custom short")) Expect(rootCmd.Version).To(Equal("2.0.0")) }) - - It("should apply custom PersistentPreRun hook", func() { - hookCalled := false - rootCmd := cli.BuildRootCommand(ctx, - cli.WithPersistentPreRun(func(cmd *cobra.Command, args []string) { - hookCalled = true - }), - ) - - Expect(rootCmd.PersistentPreRun).NotTo(BeNil()) - rootCmd.PersistentPreRun(rootCmd, []string{}) - Expect(hookCalled).To(BeTrue()) - }) - - It("should apply custom PersistentPostRun hook", func() { - hookCalled := false - rootCmd := cli.BuildRootCommand(ctx, - cli.WithPersistentPostRun(func(cmd *cobra.Command, args []string) { - hookCalled = true - }), - ) - - Expect(rootCmd.PersistentPostRun).NotTo(BeNil()) - rootCmd.PersistentPostRun(rootCmd, []string{}) - Expect(hookCalled).To(BeTrue()) - }) }) var _ = Describe("RegisterAllCommands", func() { @@ -123,7 +97,7 @@ var _ = Describe("RegisterAllCommands", func() { cli.RegisterAllCommands(ctx, rootCmd) commands := rootCmd.Commands() - Expect(len(commands)).To(BeNumerically(">", 0)) + Expect(commands).ToNot(BeEmpty()) // Check for key commands commandNames := make(map[string]bool) @@ -136,84 +110,3 @@ var _ = Describe("RegisterAllCommands", func() { Expect(commandNames).To(HaveKey("config")) }) }) - -var _ = Describe("Individual Command Builders", func() { - var ctx *context.Context - - BeforeEach(func() { - bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) - ctx = context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) - }) - - AfterEach(func() { - if ctx != nil { - ctx.Finalize() - } - }) - - It("should build exec command", func() { - cmd := cli.BuildExecCommand(ctx) - Expect(cmd).NotTo(BeNil()) - Expect(cmd.Name()).To(Equal("exec")) - }) - - It("should build browse command", func() { - cmd := cli.BuildBrowseCommand(ctx) - Expect(cmd).NotTo(BeNil()) - Expect(cmd.Name()).To(Equal("browse")) - }) - - It("should build config command", func() { - cmd := cli.BuildConfigCommand(ctx) - Expect(cmd).NotTo(BeNil()) - Expect(cmd.Name()).To(Equal("config")) - }) - - It("should build secret command", func() { - cmd := cli.BuildSecretCommand(ctx) - Expect(cmd).NotTo(BeNil()) - Expect(cmd.Name()).To(Equal("secret")) - }) - - It("should build vault command", func() { - cmd := cli.BuildVaultCommand(ctx) - Expect(cmd).NotTo(BeNil()) - Expect(cmd.Name()).To(Equal("vault")) - }) - - It("should build cache command", func() { - cmd := cli.BuildCacheCommand(ctx) - Expect(cmd).NotTo(BeNil()) - Expect(cmd.Name()).To(Equal("cache")) - }) - - It("should build workspace command", func() { - cmd := cli.BuildWorkspaceCommand(ctx) - Expect(cmd).NotTo(BeNil()) - Expect(cmd.Name()).To(Equal("workspace")) - }) - - It("should build template command", func() { - cmd := cli.BuildTemplateCommand(ctx) - Expect(cmd).NotTo(BeNil()) - Expect(cmd.Name()).To(Equal("template")) - }) - - It("should build logs command", func() { - cmd := cli.BuildLogsCommand(ctx) - Expect(cmd).NotTo(BeNil()) - Expect(cmd.Name()).To(Equal("logs")) - }) - - It("should build sync command", func() { - cmd := cli.BuildSyncCommand(ctx) - Expect(cmd).NotTo(BeNil()) - Expect(cmd.Name()).To(Equal("sync")) - }) - - It("should build mcp command", func() { - cmd := cli.BuildMCPCommand(ctx) - Expect(cmd).NotTo(BeNil()) - Expect(cmd.Name()).To(Equal("mcp")) - }) -}) diff --git a/pkg/cli/doc.go b/pkg/cli/doc.go index a6724c9c..69c2d4bd 100644 --- a/pkg/cli/doc.go +++ b/pkg/cli/doc.go @@ -56,10 +56,4 @@ // // This package is not thread-safe. Command building and modification should be // done during CLI initialization, not concurrently. -// -// # Versioning -// -// This package follows semantic versioning. The API is considered stable but may -// receive additions in minor version updates. Breaking changes will only occur -// in major version updates. package cli diff --git a/pkg/cli/examples/basic/main.go b/pkg/cli/example/main.go similarity index 50% rename from pkg/cli/examples/basic/main.go rename to pkg/cli/example/main.go index fceb3544..a314c95f 100644 --- a/pkg/cli/examples/basic/main.go +++ b/pkg/cli/example/main.go @@ -10,8 +10,10 @@ package main import ( stdCtx "context" "fmt" + "os" + + "github.com/spf13/cobra" - "github.com/flowexec/flow/internal/io" "github.com/flowexec/flow/pkg/cli" "github.com/flowexec/flow/pkg/context" "github.com/flowexec/flow/pkg/filesystem" @@ -27,16 +29,16 @@ func main() { // Initialize logger loggerOpts := logger.InitOptions{ - StdOut: io.Stdout, + StdOut: os.Stdout, LogMode: cfg.DefaultLogMode, - Theme: io.Theme(cfg.Theme.String()), + Theme: logger.Theme(cfg.Theme.String()), } logger.Init(loggerOpts) defer logger.Log().Flush() // Create context bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) - ctx := context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) + ctx := context.NewContext(bkgCtx, cancelFunc, os.Stdin, os.Stdout) defer ctx.Finalize() // Build root command with custom version @@ -48,6 +50,36 @@ func main() { // Register all Flow commands cli.RegisterAllCommands(ctx, rootCmd) + // Add a persistent hook to the root for global initialization/cleanup + cli.AddPersistentPreRunHook(rootCmd, func(cmd *cobra.Command, args []string) { + logger.Log().Debugf("Global PreRun: Initializing resources") + }) + + cli.AddPersistentPostRunHook(rootCmd, func(cmd *cobra.Command, args []string) { + logger.Log().Debugf("Global PostRun: Cleaning up resources") + }) + + // Add a custom command + premiumCmd := &cobra.Command{ + Use: "premium", + Short: "Premium features", + Long: "Access premium features of the Flow CLI", + Run: func(cmd *cobra.Command, args []string) { + logger.Log().PlainTextInfo("Welcome to Premium Flow CLI!") + logger.Log().PlainTextInfo("This is a custom command added via the extension API.") + }, + } + + rootCmd.AddCommand(premiumCmd) + + // Walk all commands and add annotations + cli.WalkCommands(rootCmd, func(cmd *cobra.Command) { + if cmd.Annotations == nil { + cmd.Annotations = make(map[string]string) + } + cmd.Annotations["edition"] = "customized" + }) + // Execute if err := cli.Execute(ctx, rootCmd); err != nil { logger.Log().FatalErr(err) diff --git a/pkg/cli/examples/hooks/main.go b/pkg/cli/examples/hooks/main.go deleted file mode 100644 index a588b69b..00000000 --- a/pkg/cli/examples/hooks/main.go +++ /dev/null @@ -1,89 +0,0 @@ -// Package main demonstrates hook injection using the Flow CLI extension API. -// -// This example shows how to: -// - Add telemetry/logging hooks to all commands -// - Use PreRun and PostRun hooks -// - Apply hooks recursively -package main - -import ( - stdCtx "context" - "fmt" - "time" - - "github.com/spf13/cobra" - - "github.com/flowexec/flow/internal/io" - "github.com/flowexec/flow/pkg/cli" - "github.com/flowexec/flow/pkg/context" - "github.com/flowexec/flow/pkg/filesystem" - "github.com/flowexec/flow/pkg/logger" -) - -// commandTiming stores timing information for commands -var commandTiming = make(map[string]time.Time) - -func main() { - // Load Flow configuration - cfg, err := filesystem.LoadConfig() - if err != nil { - panic(fmt.Errorf("user config load error: %w", err)) - } - - // Initialize logger - loggerOpts := logger.InitOptions{ - StdOut: io.Stdout, - LogMode: cfg.DefaultLogMode, - Theme: io.Theme(cfg.Theme.String()), - } - logger.Init(loggerOpts) - defer logger.Log().Flush() - - // Create context - bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) - ctx := context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) - defer ctx.Finalize() - - // Build root command - rootCmd := cli.BuildRootCommand(ctx, - cli.WithVersion("1.0.0-hooks-example"), - cli.WithShort("Flow CLI - Hook Injection Example"), - ) - - // Register all Flow commands - cli.RegisterAllCommands(ctx, rootCmd) - - // Add telemetry hooks to all commands - cli.ApplyHooksRecursive(rootCmd, - // PreRun: Start timing and log command execution - func(cmd *cobra.Command, args []string) { - commandTiming[cmd.Name()] = time.Now() - logger.Log().Infof("Starting command: %s", cmd.Name()) - if len(args) > 0 { - logger.Log().Debugf("Arguments: %v", args) - } - }, - // PostRun: Log completion and timing - func(cmd *cobra.Command, args []string) { - if startTime, ok := commandTiming[cmd.Name()]; ok { - duration := time.Since(startTime) - logger.Log().Infof("Completed command: %s (took %v)", cmd.Name(), duration) - delete(commandTiming, cmd.Name()) - } - }, - ) - - // Add a persistent hook to the root for global initialization/cleanup - cli.AddPersistentPreRunHook(rootCmd, func(cmd *cobra.Command, args []string) { - logger.Log().Debugf("Global PreRun: Initializing resources") - }) - - cli.AddPersistentPostRunHook(rootCmd, func(cmd *cobra.Command, args []string) { - logger.Log().Debugf("Global PostRun: Cleaning up resources") - }) - - // Execute - if err := cli.Execute(ctx, rootCmd); err != nil { - logger.Log().FatalErr(err) - } -} diff --git a/pkg/cli/examples/override/main.go b/pkg/cli/examples/override/main.go deleted file mode 100644 index d6ab4bee..00000000 --- a/pkg/cli/examples/override/main.go +++ /dev/null @@ -1,106 +0,0 @@ -// Package main demonstrates command overriding using the Flow CLI extension API. -// -// This example shows how to: -// - Replace existing commands with custom implementations -// - Add new commands to the CLI -// - Customize command behavior -package main - -import ( - stdCtx "context" - "fmt" - - "github.com/spf13/cobra" - - "github.com/flowexec/flow/internal/io" - "github.com/flowexec/flow/pkg/cli" - "github.com/flowexec/flow/pkg/context" - "github.com/flowexec/flow/pkg/filesystem" - "github.com/flowexec/flow/pkg/logger" -) - -func main() { - // Load Flow configuration - cfg, err := filesystem.LoadConfig() - if err != nil { - panic(fmt.Errorf("user config load error: %w", err)) - } - - // Initialize logger - loggerOpts := logger.InitOptions{ - StdOut: io.Stdout, - LogMode: cfg.DefaultLogMode, - Theme: io.Theme(cfg.Theme.String()), - } - logger.Init(loggerOpts) - defer logger.Log().Flush() - - // Create context - bkgCtx, cancelFunc := stdCtx.WithCancel(stdCtx.Background()) - ctx := context.NewContext(bkgCtx, cancelFunc, io.Stdin, io.Stdout) - defer ctx.Finalize() - - // Build root command - rootCmd := cli.BuildRootCommand(ctx, - cli.WithVersion("1.0.0-override-example"), - cli.WithShort("Flow CLI - Command Override Example"), - ) - - // Register all Flow commands - cli.RegisterAllCommands(ctx, rootCmd) - - // Add a custom "premium" command - premiumCmd := &cobra.Command{ - Use: "premium", - Short: "Premium features", - Long: "Access premium features of the Flow CLI", - Run: func(cmd *cobra.Command, args []string) { - logger.Log().PlainTextInfo("Welcome to Premium Flow CLI!") - logger.Log().PlainTextInfo("This is a custom command added via the extension API.") - }, - } - - // Add subcommands to the premium command - premiumCmd.AddCommand(&cobra.Command{ - Use: "status", - Short: "Check premium status", - Run: func(cmd *cobra.Command, args []string) { - logger.Log().PlainTextInfo("Premium Status: Active") - logger.Log().PlainTextInfo("License: Enterprise") - logger.Log().PlainTextInfo("Expiry: Never") - }, - }) - - rootCmd.AddCommand(premiumCmd) - - // Override the version command with custom behavior - // First, let's add a custom version command that shows additional info - customVersionCmd := &cobra.Command{ - Use: "version", - Short: "Show version information with premium details", - Run: func(cmd *cobra.Command, args []string) { - logger.Log().PlainTextInfo("Flow CLI Version: 1.0.0-override-example") - logger.Log().PlainTextInfo("Edition: Premium") - logger.Log().PlainTextInfo("Build Date: 2025-12-26") - logger.Log().PlainTextInfo("License: Enterprise") - }, - } - - // Since version is built into the root command via --version flag, - // we'll add it as a subcommand instead - rootCmd.AddCommand(customVersionCmd) - - // Walk all commands and add annotations - cli.WalkCommands(rootCmd, func(cmd *cobra.Command) { - if cmd.Annotations == nil { - cmd.Annotations = make(map[string]string) - } - cmd.Annotations["edition"] = "premium" - cmd.Annotations["customized"] = "true" - }) - - // Execute - if err := cli.Execute(ctx, rootCmd); err != nil { - logger.Log().FatalErr(err) - } -} diff --git a/pkg/cli/hooks.go b/pkg/cli/hooks.go index c4b67388..047fd45b 100644 --- a/pkg/cli/hooks.go +++ b/pkg/cli/hooks.go @@ -4,12 +4,6 @@ import "github.com/spf13/cobra" // AddPreRunHook adds a PreRun hook to a command, preserving any existing PreRun hook. // If the command already has a PreRun hook, the new hook will run before it. -// -// Example: -// -// cli.AddPreRunHook(cmd, func(cmd *cobra.Command, args []string) { -// log.Println("Starting command:", cmd.Name()) -// }) func AddPreRunHook(cmd *cobra.Command, hook HookFunc) { if cmd == nil || hook == nil { return @@ -21,7 +15,6 @@ func AddPreRunHook(cmd *cobra.Command, hook HookFunc) { return } - // Chain the hooks: new hook runs first, then existing hook cmd.PreRun = func(c *cobra.Command, args []string) { hook(c, args) existingHook(c, args) @@ -30,12 +23,6 @@ func AddPreRunHook(cmd *cobra.Command, hook HookFunc) { // AddPostRunHook adds a PostRun hook to a command, preserving any existing PostRun hook. // If the command already has a PostRun hook, the new hook will run after it. -// -// Example: -// -// cli.AddPostRunHook(cmd, func(cmd *cobra.Command, args []string) { -// log.Println("Finished command:", cmd.Name()) -// }) func AddPostRunHook(cmd *cobra.Command, hook HookFunc) { if cmd == nil || hook == nil { return @@ -47,7 +34,6 @@ func AddPostRunHook(cmd *cobra.Command, hook HookFunc) { return } - // Chain the hooks: existing hook runs first, then new hook cmd.PostRun = func(c *cobra.Command, args []string) { existingHook(c, args) hook(c, args) @@ -58,12 +44,6 @@ func AddPostRunHook(cmd *cobra.Command, hook HookFunc) { // If the command already has a PersistentPreRun hook, the new hook will run before it. // // PersistentPreRun hooks run before all commands in the subtree. -// -// Example: -// -// cli.AddPersistentPreRunHook(rootCmd, func(cmd *cobra.Command, args []string) { -// log.Println("Initializing...") -// }) func AddPersistentPreRunHook(cmd *cobra.Command, hook HookFunc) { if cmd == nil || hook == nil { return @@ -75,7 +55,6 @@ func AddPersistentPreRunHook(cmd *cobra.Command, hook HookFunc) { return } - // Chain the hooks: new hook runs first, then existing hook cmd.PersistentPreRun = func(c *cobra.Command, args []string) { hook(c, args) existingHook(c, args) @@ -86,12 +65,6 @@ func AddPersistentPreRunHook(cmd *cobra.Command, hook HookFunc) { // If the command already has a PersistentPostRun hook, the new hook will run after it. // // PersistentPostRun hooks run after all commands in the subtree. -// -// Example: -// -// cli.AddPersistentPostRunHook(rootCmd, func(cmd *cobra.Command, args []string) { -// log.Println("Cleaning up...") -// }) func AddPersistentPostRunHook(cmd *cobra.Command, hook HookFunc) { if cmd == nil || hook == nil { return @@ -103,143 +76,8 @@ func AddPersistentPostRunHook(cmd *cobra.Command, hook HookFunc) { return } - // Chain the hooks: existing hook runs first, then new hook cmd.PersistentPostRun = func(c *cobra.Command, args []string) { existingHook(c, args) hook(c, args) } } - -// ApplyHooksRecursive applies PreRun and PostRun hooks to a command and all its subcommands. -// This is useful for adding cross-cutting concerns like telemetry or logging to all commands. -// -// Pass nil for either preRun or postRun to skip adding that hook type. -// -// Example: -// -// cli.ApplyHooksRecursive(rootCmd, -// func(cmd *cobra.Command, args []string) { -// telemetry.Start(cmd.Name()) -// }, -// func(cmd *cobra.Command, args []string) { -// telemetry.End(cmd.Name()) -// }, -// ) -func ApplyHooksRecursive(cmd *cobra.Command, preRun, postRun HookFunc) { - if cmd == nil { - return - } - - // Add hooks to this command - if preRun != nil { - AddPreRunHook(cmd, preRun) - } - if postRun != nil { - AddPostRunHook(cmd, postRun) - } - - // Recursively apply to all subcommands - for _, subCmd := range cmd.Commands() { - ApplyHooksRecursive(subCmd, preRun, postRun) - } -} - -// ApplyPersistentHooksRecursive applies PersistentPreRun and PersistentPostRun hooks -// to a command and all its subcommands. -// -// Pass nil for either preRun or postRun to skip adding that hook type. -// -// Example: -// -// cli.ApplyPersistentHooksRecursive(rootCmd, -// func(cmd *cobra.Command, args []string) { -// // Initialize resources -// }, -// func(cmd *cobra.Command, args []string) { -// // Clean up resources -// }, -// ) -func ApplyPersistentHooksRecursive(cmd *cobra.Command, preRun, postRun HookFunc) { - if cmd == nil { - return - } - - // Add hooks to this command - if preRun != nil { - AddPersistentPreRunHook(cmd, preRun) - } - if postRun != nil { - AddPersistentPostRunHook(cmd, postRun) - } - - // Recursively apply to all subcommands - for _, subCmd := range cmd.Commands() { - ApplyPersistentHooksRecursive(subCmd, preRun, postRun) - } -} - -// WrapRunFunc wraps a command's Run function with before and after hooks. -// This is useful when you want to wrap the actual command execution logic -// rather than using PreRun/PostRun. -// -// Pass nil for either before or after to skip that hook. -// -// Example: -// -// cli.WrapRunFunc(cmd, -// func(cmd *cobra.Command, args []string) { -// log.Println("Before run") -// }, -// func(cmd *cobra.Command, args []string) { -// log.Println("After run") -// }, -// ) -func WrapRunFunc(cmd *cobra.Command, before, after HookFunc) { - if cmd == nil || cmd.Run == nil { - return - } - - existingRun := cmd.Run - cmd.Run = func(c *cobra.Command, args []string) { - if before != nil { - before(c, args) - } - existingRun(c, args) - if after != nil { - after(c, args) - } - } -} - -// WrapRunEFunc wraps a command's RunE function with before and after hooks. -// This is similar to WrapRunFunc but for commands that return errors. -// -// Pass nil for either before or after to skip that hook. -// -// Example: -// -// cli.WrapRunEFunc(cmd, -// func(cmd *cobra.Command, args []string) { -// log.Println("Before run") -// }, -// func(cmd *cobra.Command, args []string) { -// log.Println("After run") -// }, -// ) -func WrapRunEFunc(cmd *cobra.Command, before, after HookFunc) { - if cmd == nil || cmd.RunE == nil { - return - } - - existingRunE := cmd.RunE - cmd.RunE = func(c *cobra.Command, args []string) error { - if before != nil { - before(c, args) - } - err := existingRunE(c, args) - if after != nil { - after(c, args) - } - return err - } -} diff --git a/pkg/cli/hooks_test.go b/pkg/cli/hooks_test.go index 3e0dacb0..259278a0 100644 --- a/pkg/cli/hooks_test.go +++ b/pkg/cli/hooks_test.go @@ -157,167 +157,4 @@ var _ = Describe("Hook Injection", func() { Expect(callOrder).To(Equal([]int{1, 2})) }) }) - - Describe("ApplyHooksRecursive", func() { - It("should apply hooks to root and all subcommands", func() { - rootCmd := &cobra.Command{Use: "root"} - subCmd1 := &cobra.Command{Use: "sub1"} - subCmd2 := &cobra.Command{Use: "sub2"} - rootCmd.AddCommand(subCmd1, subCmd2) - - preRunCalls := []string{} - postRunCalls := []string{} - - cli.ApplyHooksRecursive(rootCmd, - func(c *cobra.Command, args []string) { - preRunCalls = append(preRunCalls, c.Use) - }, - func(c *cobra.Command, args []string) { - postRunCalls = append(postRunCalls, c.Use) - }, - ) - - // Verify hooks were added - Expect(rootCmd.PreRun).NotTo(BeNil()) - Expect(rootCmd.PostRun).NotTo(BeNil()) - Expect(subCmd1.PreRun).NotTo(BeNil()) - Expect(subCmd1.PostRun).NotTo(BeNil()) - Expect(subCmd2.PreRun).NotTo(BeNil()) - Expect(subCmd2.PostRun).NotTo(BeNil()) - - // Execute hooks - rootCmd.PreRun(rootCmd, []string{}) - subCmd1.PreRun(subCmd1, []string{}) - subCmd2.PreRun(subCmd2, []string{}) - - Expect(preRunCalls).To(ContainElements("root", "sub1", "sub2")) - - rootCmd.PostRun(rootCmd, []string{}) - subCmd1.PostRun(subCmd1, []string{}) - subCmd2.PostRun(subCmd2, []string{}) - - Expect(postRunCalls).To(ContainElements("root", "sub1", "sub2")) - }) - - It("should handle nil hooks gracefully", func() { - cmd := &cobra.Command{Use: "test"} - - Expect(func() { - cli.ApplyHooksRecursive(cmd, nil, nil) - }).NotTo(Panic()) - }) - - It("should handle nil command gracefully", func() { - Expect(func() { - cli.ApplyHooksRecursive(nil, func(c *cobra.Command, args []string) {}, nil) - }).NotTo(Panic()) - }) - }) - - Describe("ApplyPersistentHooksRecursive", func() { - It("should apply persistent hooks recursively", func() { - rootCmd := &cobra.Command{Use: "root"} - subCmd := &cobra.Command{Use: "sub"} - rootCmd.AddCommand(subCmd) - - hookCalled := false - - cli.ApplyPersistentHooksRecursive(rootCmd, - func(c *cobra.Command, args []string) { - hookCalled = true - }, - nil, - ) - - Expect(rootCmd.PersistentPreRun).NotTo(BeNil()) - Expect(subCmd.PersistentPreRun).NotTo(BeNil()) - - rootCmd.PersistentPreRun(rootCmd, []string{}) - Expect(hookCalled).To(BeTrue()) - }) - }) - - Describe("WrapRunFunc", func() { - It("should wrap Run function with before and after hooks", func() { - callOrder := []string{} - cmd := &cobra.Command{ - Use: "test", - Run: func(c *cobra.Command, args []string) { - callOrder = append(callOrder, "run") - }, - } - - cli.WrapRunFunc(cmd, - func(c *cobra.Command, args []string) { - callOrder = append(callOrder, "before") - }, - func(c *cobra.Command, args []string) { - callOrder = append(callOrder, "after") - }, - ) - - cmd.Run(cmd, []string{}) - Expect(callOrder).To(Equal([]string{"before", "run", "after"})) - }) - - It("should handle nil before hook", func() { - callOrder := []string{} - cmd := &cobra.Command{ - Use: "test", - Run: func(c *cobra.Command, args []string) { - callOrder = append(callOrder, "run") - }, - } - - cli.WrapRunFunc(cmd, nil, func(c *cobra.Command, args []string) { - callOrder = append(callOrder, "after") - }) - - cmd.Run(cmd, []string{}) - Expect(callOrder).To(Equal([]string{"run", "after"})) - }) - - It("should handle nil after hook", func() { - callOrder := []string{} - cmd := &cobra.Command{ - Use: "test", - Run: func(c *cobra.Command, args []string) { - callOrder = append(callOrder, "run") - }, - } - - cli.WrapRunFunc(cmd, func(c *cobra.Command, args []string) { - callOrder = append(callOrder, "before") - }, nil) - - cmd.Run(cmd, []string{}) - Expect(callOrder).To(Equal([]string{"before", "run"})) - }) - }) - - Describe("WrapRunEFunc", func() { - It("should wrap RunE function with before and after hooks", func() { - callOrder := []string{} - cmd := &cobra.Command{ - Use: "test", - RunE: func(c *cobra.Command, args []string) error { - callOrder = append(callOrder, "run") - return nil - }, - } - - cli.WrapRunEFunc(cmd, - func(c *cobra.Command, args []string) { - callOrder = append(callOrder, "before") - }, - func(c *cobra.Command, args []string) { - callOrder = append(callOrder, "after") - }, - ) - - err := cmd.RunE(cmd, []string{}) - Expect(err).NotTo(HaveOccurred()) - Expect(callOrder).To(Equal([]string{"before", "run", "after"})) - }) - }) }) diff --git a/pkg/cli/registry.go b/pkg/cli/registry.go index 1e61e16b..61662cbb 100644 --- a/pkg/cli/registry.go +++ b/pkg/cli/registry.go @@ -4,19 +4,12 @@ import ( "fmt" "github.com/spf13/cobra" - "github.com/spf13/pflag" ) // WalkCommands traverses the command tree starting from the root and applies // a function to each command (including the root). // // The traversal is depth-first, visiting parent commands before their children. -// -// Example: -// -// cli.WalkCommands(rootCmd, func(cmd *cobra.Command) { -// fmt.Println("Command:", cmd.Name()) -// }) func WalkCommands(rootCmd *cobra.Command, fn func(*cobra.Command)) { if rootCmd == nil || fn == nil { return @@ -35,19 +28,11 @@ func WalkCommands(rootCmd *cobra.Command, fn func(*cobra.Command)) { // It performs a breadth-first search starting from the root. // // Returns nil if the command is not found. -// -// Example: -// -// wsCmd := cli.FindCommand(rootCmd, "workspace") -// if wsCmd != nil { -// // Found the workspace command -// } func FindCommand(rootCmd *cobra.Command, name string) *cobra.Command { if rootCmd == nil || name == "" { return nil } - // Check if this is the command we're looking for if rootCmd.Name() == name { return rootCmd } @@ -73,13 +58,6 @@ func FindCommand(rootCmd *cobra.Command, name string) *cobra.Command { // The path should be space-separated command names. // // Returns nil if the command is not found. -// -// Example: -// -// addCmd := cli.FindCommandPath(rootCmd, "workspace add") -// if addCmd != nil { -// // Found the workspace add command -// } func FindCommandPath(rootCmd *cobra.Command, path string) *cobra.Command { if rootCmd == nil || path == "" { return nil @@ -97,37 +75,22 @@ func FindCommandPath(rootCmd *cobra.Command, path string) *cobra.Command { // The new command will be added to the same parent as the old command. // // Returns an error if the old command is not found or if the replacement fails. -// -// Example: -// -// customExec := &cobra.Command{ -// Use: "exec", -// Run: customExecFunc, -// } -// if err := cli.ReplaceCommand(rootCmd, "exec", customExec); err != nil { -// log.Fatal(err) -// } func ReplaceCommand(rootCmd *cobra.Command, oldName string, newCmd *cobra.Command) error { if rootCmd == nil || oldName == "" || newCmd == nil { return fmt.Errorf("invalid parameters: rootCmd, oldName, and newCmd must not be nil/empty") } - // Find the command to replace oldCmd := FindCommand(rootCmd, oldName) if oldCmd == nil { return fmt.Errorf("command %q not found", oldName) } - // Get the parent of the old command parent := oldCmd.Parent() if parent == nil { return fmt.Errorf("cannot replace root command") } - // Remove the old command parent.RemoveCommand(oldCmd) - - // Add the new command parent.AddCommand(newCmd) return nil @@ -136,30 +99,21 @@ func ReplaceCommand(rootCmd *cobra.Command, oldName string, newCmd *cobra.Comman // RemoveCommand removes a command by name from the command tree. // // Returns an error if the command is not found or if it's the root command. -// -// Example: -// -// if err := cli.RemoveCommand(rootCmd, "sync"); err != nil { -// log.Fatal(err) -// } func RemoveCommand(rootCmd *cobra.Command, name string) error { if rootCmd == nil || name == "" { return fmt.Errorf("invalid parameters: rootCmd and name must not be nil/empty") } - // Find the command cmd := FindCommand(rootCmd, name) if cmd == nil { return fmt.Errorf("command %q not found", name) } - // Get the parent parent := cmd.Parent() if parent == nil { return fmt.Errorf("cannot remove root command") } - // Remove the command parent.RemoveCommand(cmd) return nil @@ -167,11 +121,6 @@ func RemoveCommand(rootCmd *cobra.Command, name string) error { // ListCommands returns a list of all command names in the tree. // The list includes the root command and all subcommands. -// -// Example: -// -// names := cli.ListCommands(rootCmd) -// fmt.Println("Available commands:", names) func ListCommands(rootCmd *cobra.Command) []string { var names []string WalkCommands(rootCmd, func(cmd *cobra.Command) { @@ -182,14 +131,6 @@ func ListCommands(rootCmd *cobra.Command) []string { // GetSubcommands returns a map of subcommand names to commands for a given command. // This is a convenience wrapper around cobra's Commands() method. -// -// Example: -// -// wsCmd := cli.FindCommand(rootCmd, "workspace") -// subCmds := cli.GetSubcommands(wsCmd) -// for name, cmd := range subCmds { -// fmt.Printf("Subcommand: %s - %s\n", name, cmd.Short) -// } func GetSubcommands(cmd *cobra.Command) map[string]*cobra.Command { if cmd == nil { return nil @@ -202,69 +143,6 @@ func GetSubcommands(cmd *cobra.Command) map[string]*cobra.Command { return subCmds } -// CloneCommand creates a shallow copy of a command. -// This is useful when you want to modify a command without affecting the original. -// -// Note: This creates a shallow copy - the Run functions and other function fields -// will reference the same functions as the original. -// -// Example: -// -// origCmd := cli.FindCommand(rootCmd, "exec") -// clonedCmd := cli.CloneCommand(origCmd) -// clonedCmd.Short = "Custom exec command" -func CloneCommand(cmd *cobra.Command) *cobra.Command { - if cmd == nil { - return nil - } - - clone := &cobra.Command{ - Use: cmd.Use, - Aliases: append([]string(nil), cmd.Aliases...), - Short: cmd.Short, - Long: cmd.Long, - Example: cmd.Example, - ValidArgs: append([]string(nil), cmd.ValidArgs...), - Args: cmd.Args, - ArgAliases: append([]string(nil), cmd.ArgAliases...), - BashCompletionFunction: cmd.BashCompletionFunction, - Deprecated: cmd.Deprecated, - Hidden: cmd.Hidden, - Annotations: copyMap(cmd.Annotations), - Version: cmd.Version, - PersistentPreRun: cmd.PersistentPreRun, - PersistentPreRunE: cmd.PersistentPreRunE, - PreRun: cmd.PreRun, - PreRunE: cmd.PreRunE, - Run: cmd.Run, - RunE: cmd.RunE, - PostRun: cmd.PostRun, - PostRunE: cmd.PostRunE, - PersistentPostRun: cmd.PersistentPostRun, - PersistentPostRunE: cmd.PersistentPostRunE, - SilenceErrors: cmd.SilenceErrors, - SilenceUsage: cmd.SilenceUsage, - DisableFlagParsing: cmd.DisableFlagParsing, - DisableAutoGenTag: cmd.DisableAutoGenTag, - DisableFlagsInUseLine: cmd.DisableFlagsInUseLine, - DisableSuggestions: cmd.DisableSuggestions, - SuggestionsMinimumDistance: cmd.SuggestionsMinimumDistance, - TraverseChildren: cmd.TraverseChildren, - FParseErrWhitelist: cmd.FParseErrWhitelist, - } - - // Copy flags - cmd.Flags().VisitAll(func(f *pflag.Flag) { - clone.Flags().AddFlag(f) - }) - cmd.PersistentFlags().VisitAll(func(f *pflag.Flag) { - clone.PersistentFlags().AddFlag(f) - }) - - return clone -} - -// Helper function to split a command path into individual command names func splitPath(path string) []string { var parts []string current := "" @@ -283,15 +161,3 @@ func splitPath(path string) []string { } return parts } - -// Helper function to copy a map -func copyMap(m map[string]string) map[string]string { - if m == nil { - return nil - } - copy := make(map[string]string, len(m)) - for k, v := range m { - copy[k] = v - } - return copy -} diff --git a/pkg/cli/registry_test.go b/pkg/cli/registry_test.go index c2fe6ffe..c363519e 100644 --- a/pkg/cli/registry_test.go +++ b/pkg/cli/registry_test.go @@ -1,8 +1,6 @@ package cli_test import ( - "fmt" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/spf13/cobra" @@ -244,50 +242,4 @@ var _ = Describe("Command Registry", func() { Expect(subCmds).To(BeNil()) }) }) - - Describe("CloneCommand", func() { - It("should create a shallow copy of command", func() { - original := &cobra.Command{ - Use: "test", - Short: "Test command", - Long: "Long description", - Aliases: []string{"t", "tst"}, - Run: func(cmd *cobra.Command, args []string) { - fmt.Println("Running") - }, - } - - clone := cli.CloneCommand(original) - Expect(clone).NotTo(BeNil()) - Expect(clone.Use).To(Equal(original.Use)) - Expect(clone.Short).To(Equal(original.Short)) - Expect(clone.Long).To(Equal(original.Long)) - Expect(clone.Aliases).To(Equal(original.Aliases)) - Expect(clone.Run).NotTo(BeNil()) - }) - - It("should handle nil command", func() { - clone := cli.CloneCommand(nil) - Expect(clone).To(BeNil()) - }) - - It("should copy annotations", func() { - original := &cobra.Command{ - Use: "test", - Annotations: map[string]string{ - "key1": "value1", - "key2": "value2", - }, - } - - clone := cli.CloneCommand(original) - Expect(clone.Annotations).To(HaveLen(2)) - Expect(clone.Annotations["key1"]).To(Equal("value1")) - Expect(clone.Annotations["key2"]).To(Equal("value2")) - - // Modify clone's annotations shouldn't affect original - clone.Annotations["key3"] = "value3" - Expect(original.Annotations).NotTo(HaveKey("key3")) - }) - }) }) diff --git a/pkg/cli/types.go b/pkg/cli/types.go index d62ad38e..79f15ef3 100644 --- a/pkg/cli/types.go +++ b/pkg/cli/types.go @@ -19,12 +19,6 @@ type RootConfig struct { // Version is the version string (defaults to Flow's current version) Version string - - // PersistentPreRun is a hook that runs before all commands - PersistentPreRun HookFunc - - // PersistentPostRun is a hook that runs after all commands - PersistentPostRun HookFunc } // RootOption is a functional option for configuring the root command. @@ -57,23 +51,3 @@ func WithVersion(version string) RootOption { c.Version = version } } - -// WithPersistentPreRun sets a PersistentPreRun hook for the root command. -// This hook will run before all commands in the tree. -// Note: This will replace any existing PersistentPreRun hook from the default -// root command. Use AddPersistentPreRunHook if you want to chain hooks. -func WithPersistentPreRun(hook HookFunc) RootOption { - return func(c *RootConfig) { - c.PersistentPreRun = hook - } -} - -// WithPersistentPostRun sets a PersistentPostRun hook for the root command. -// This hook will run after all commands in the tree. -// Note: This will replace any existing PersistentPostRun hook from the default -// root command. Use AddPersistentPostRunHook if you want to chain hooks. -func WithPersistentPostRun(hook HookFunc) RootOption { - return func(c *RootConfig) { - c.PersistentPostRun = hook - } -} diff --git a/pkg/context/context.go b/pkg/context/context.go index e36ed139..a686694b 100644 --- a/pkg/context/context.go +++ b/pkg/context/context.go @@ -11,7 +11,6 @@ import ( "github.com/flowexec/tuikit/themes" "github.com/pkg/errors" - flowIO "github.com/flowexec/flow/internal/io" "github.com/flowexec/flow/pkg/cache" "github.com/flowexec/flow/pkg/filesystem" "github.com/flowexec/flow/pkg/logger" @@ -85,7 +84,7 @@ func NewContext(ctx context.Context, cancelFunc context.CancelFunc, stdIn, stdOu tuikit.WithLoadingMsg("thinking..."), ) - theme := flowIO.Theme(cfg.Theme.String()) + theme := logger.Theme(cfg.Theme.String()) if cfg.ColorOverride != nil { theme = overrideThemeColor(theme, cfg.ColorOverride) } diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 5163e5f4..f8e868fa 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -5,12 +5,15 @@ import ( ) type ExecutableNotFoundError struct { - Verb string - Name string + Ref string } func (e ExecutableNotFoundError) Error() string { - return fmt.Sprintf("%s executable %s not found", e.Verb, e.Name) + return fmt.Sprintf("%s executable not found", e.Ref) +} + +func NewExecutableNotFoundError(ref string) ExecutableNotFoundError { + return ExecutableNotFoundError{Ref: ref} } type WorkspaceNotFoundError struct { @@ -34,3 +37,19 @@ func (e ExecutableContextError) Error() string { e.FlowFile, ) } + +type CacheUpdateError struct { + Err error +} + +func (e CacheUpdateError) Error() string { + return fmt.Sprintf("unable to update cache - %v", e.Err) +} + +func (e CacheUpdateError) Unwrap() error { + return e.Err +} + +func NewCacheUpdateError(err error) CacheUpdateError { + return CacheUpdateError{Err: err} +} diff --git a/internal/io/styles.go b/pkg/logger/theme.go similarity index 93% rename from internal/io/styles.go rename to pkg/logger/theme.go index c3d6553d..974bc832 100644 --- a/internal/io/styles.go +++ b/pkg/logger/theme.go @@ -1,4 +1,4 @@ -package io +package logger import "github.com/flowexec/tuikit/themes" diff --git a/tests/browse_cmds_e2e_test.go b/tests/browse_cmds_e2e_test.go index 401970ad..231fed70 100644 --- a/tests/browse_cmds_e2e_test.go +++ b/tests/browse_cmds_e2e_test.go @@ -15,9 +15,9 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/flowexec/flow/internal/io" execIO "github.com/flowexec/flow/internal/io/executable" "github.com/flowexec/flow/internal/io/library" + "github.com/flowexec/flow/pkg/logger" "github.com/flowexec/flow/tests/utils" "github.com/flowexec/flow/types/executable" ) @@ -60,7 +60,7 @@ var _ = Describe("browse TUI", func() { libraryView := library.NewLibraryView( ctx.Context, wsList, execList, library.Filter{}, - io.Theme(ctx.Config.Theme.String()), + logger.Theme(ctx.Config.Theme.String()), runFunc, ) Expect(container.SetView(libraryView)).To(Succeed()) @@ -89,7 +89,7 @@ var _ = Describe("browse TUI", func() { libraryView := library.NewLibraryView( ctx.Context, wsList, execList, library.Filter{}, - io.Theme(ctx.Config.Theme.String()), + logger.Theme(ctx.Config.Theme.String()), runFunc, ) Expect(container.SetView(libraryView)).To(Succeed()) diff --git a/tests/utils/context.go b/tests/utils/context.go index 0cbf4e82..ea02098e 100644 --- a/tests/utils/context.go +++ b/tests/utils/context.go @@ -14,7 +14,6 @@ import ( "go.uber.org/mock/gomock" "gopkg.in/yaml.v3" - "github.com/flowexec/flow/internal/io" "github.com/flowexec/flow/internal/runner/mocks" "github.com/flowexec/flow/internal/services/store" "github.com/flowexec/flow/pkg/cache" @@ -54,7 +53,7 @@ func NewContext(ctx stdCtx.Context, tb testing.TB) *Context { stdOut, stdIn := createTempIOFiles(tb) tempLogger := tuikitIO.NewLogger( tuikitIO.WithOutput(stdOut), - tuikitIO.WithTheme(io.Theme("")), + tuikitIO.WithTheme(logger.Theme("")), tuikitIO.WithMode(tuikitIO.Text), tuikitIO.WithExitFunc(func(msg string, args ...any) { msg = fmt.Sprintf(msg, args...) @@ -126,7 +125,7 @@ func ResetTestContext(ctx *Context, tb testing.TB) { setTestEnv(tb, ctx.configDir, ctx.cacheDir) newLogger := tuikitIO.NewLogger( tuikitIO.WithOutput(stdOut), - tuikitIO.WithTheme(io.Theme("")), + tuikitIO.WithTheme(logger.Theme("")), tuikitIO.WithMode(tuikitIO.Text), tuikitIO.WithExitFunc(func(msg string, args ...any) { msg = fmt.Sprintf(msg, args...) diff --git a/tests/utils/runner.go b/tests/utils/runner.go index a39292af..c532767b 100644 --- a/tests/utils/runner.go +++ b/tests/utils/runner.go @@ -6,6 +6,7 @@ import ( "os/exec" "github.com/flowexec/flow/cmd" + "github.com/flowexec/flow/pkg/cli" "github.com/flowexec/flow/pkg/context" ) @@ -21,7 +22,8 @@ func (r *CommandRunner) Run(ctx *context.Context, args ...string) (err error) { err = fmt.Errorf("panic occurred: %v", r) } }() - rootCmd := cmd.NewRootCmd(ctx) + rootCmd := cli.BuildRootCommand(ctx) + cli.RegisterAllCommands(ctx, rootCmd) rootCmd.SetArgs(args) rootCmd.SetIn(ctx.StdIn()) rootCmd.SetOut(ctx.StdOut()) diff --git a/tools/docsgen/main.go b/tools/docsgen/main.go index 287d5e0f..3391e3f1 100644 --- a/tools/docsgen/main.go +++ b/tools/docsgen/main.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/cobra/doc" - "github.com/flowexec/flow/cmd" + "github.com/flowexec/flow/pkg/cli" "github.com/flowexec/flow/pkg/context" ) @@ -24,8 +24,8 @@ func main() { ctx := context.NewContext(bkgCtx, cancelFunc, os.Stdin, os.Stdout) defer ctx.Finalize() - rootCmd := cmd.NewRootCmd(ctx) - cmd.RegisterSubCommands(ctx, rootCmd) + rootCmd := cli.BuildRootCommand(ctx) + cli.RegisterAllCommands(ctx, rootCmd) rootCmd.DisableAutoGenTag = true if err := doc.GenMarkdownTree(rootCmd, filepath.Join(rootDir(), DocsDir, cliDir)); err != nil { panic(err) diff --git a/types/executable/executable.go b/types/executable/executable.go index f1c7e756..d154f5e5 100644 --- a/types/executable/executable.go +++ b/types/executable/executable.go @@ -391,7 +391,7 @@ func (l ExecutableList) FindByVerbAndID(verb Verb, id string) (*Executable, erro if exec != nil { return exec, nil } - return nil, errors.ExecutableNotFoundError{Verb: string(verb), Name: name} + return nil, errors.NewExecutableNotFoundError(NewRef(name, verb).String()) } func (l ExecutableList) FilterByTags(tags common.Tags) ExecutableList {