11package main
22
33import (
4+ "context"
5+ _ "embed"
46 "fmt"
7+ "io"
8+ "log/slog"
59 "os"
10+ "os/signal"
11+ "syscall"
612 "time"
13+
14+ workflow "github.com/GoCodeAlone/workflow"
15+ "github.com/GoCodeAlone/workflow/config"
16+ "github.com/GoCodeAlone/workflow/handlers"
17+ "github.com/GoCodeAlone/workflow/module"
718)
819
20+ // wfctlConfigBytes is the embedded workflow config that declares wfctl's CLI
21+ // structure and maps every command to a pipeline triggered via the "cli"
22+ // trigger type. The engine resolves these pipelines at startup so each command
23+ // flows through the workflow engine as a proper workflow primitive.
24+ //
25+ //go:embed wfctl.yaml
26+ var wfctlConfigBytes []byte
27+
928var version = "dev"
1029
30+ // commands maps each CLI command name to its Go implementation. The command
31+ // metadata (name, description) is declared in wfctl.yaml; this map provides
32+ // the runtime functions that are registered in the CLICommandRegistry service
33+ // and invoked by step.cli_invoke from within each command's pipeline.
1134var commands = map [string ]func ([]string ) error {
1235 "init" : runInit ,
1336 "validate" : runValidate ,
@@ -35,65 +58,83 @@ var commands = map[string]func([]string) error{
3558 "mcp" : runMCP ,
3659}
3760
38- func usage () {
39- fmt .Fprintf (os .Stderr , `wfctl - Workflow Engine CLI (version %s)
40-
41- Usage:
42- wfctl <command> [options]
43-
44- Commands:
45- init Scaffold a new workflow project from a template
46- validate Validate a workflow configuration file
47- inspect Inspect modules, workflows, and triggers in a config
48- run Run a workflow engine from a config file
49- plugin Plugin management (init, docs, search, install, list, update, remove)
50- pipeline Pipeline management (list, run)
51- schema Generate JSON Schema for workflow configs
52- snippets Export IDE snippets (--format vscode|jetbrains|json)
53- manifest Analyze config and report infrastructure requirements
54- migrate Manage database schema migrations
55- build-ui Build the application UI (npm install + npm run build + validate)
56- ui UI tooling (scaffold: generate Vite+React+TypeScript SPA from OpenAPI spec)
57- publish Prepare and publish a plugin manifest to the workflow-registry
58- deploy Deploy the workflow application (docker, kubernetes, cloud)
59- api API tooling (extract: generate OpenAPI 3.0 spec from config)
60- diff Compare two workflow config files and show what changed
61- template Template management (validate: check templates against known types)
62- contract Contract testing (test: generate/compare API contracts)
63- compat Compatibility checking (check: verify config works with current engine)
64- generate Code generation (github-actions: generate CI/CD workflows from config)
65- git Git integration (connect: link to GitHub repo, push: commit and push)
66- registry Registry management (list, add, remove plugin registry sources)
67- update Update wfctl to the latest version (use --check to only check)
68- mcp Start the MCP server over stdio for AI assistant integration
69-
70- Run 'wfctl <command> -h' for command-specific help.
71- ` , version )
72- }
73-
7461func main () {
75- if len (os .Args ) < 2 {
76- usage ()
62+ // Load the embedded config. All command definitions and pipeline wiring
63+ // live in wfctl.yaml — no hardcoded routing in this file.
64+ cfg , err := config .LoadFromBytes (wfctlConfigBytes )
65+ if err != nil {
66+ fmt .Fprintf (os .Stderr , "internal error: failed to load embedded config: %v\n " , err ) //nolint:gosec // G705
7767 os .Exit (1 )
7868 }
7969
80- cmd := os . Args [ 1 ]
81- if cmd == "-h" || cmd == "--help" || cmd == "help" {
82- usage ()
83- os . Exit ( 0 )
70+ // Inject the build-time version into the cli workflow config map so that
71+ // --version and the usage header display the correct release string.
72+ if wfCfg , ok := cfg . Workflows [ "cli" ].( map [ string ] any ); ok {
73+ wfCfg [ "version" ] = version
8474 }
85- if cmd == "-v" || cmd == "--version" || cmd == "version" {
86- fmt .Println (version )
87- os .Exit (0 )
75+
76+ // Build the engine with all default handlers and triggers.
77+ // The discard logger is propagated to all cmd-* pipelines automatically
78+ // via configurePipelines, so internal plumbing logs do not appear in the
79+ // terminal. Each command creates its own logger when it needs output.
80+ engineLogger := slog .New (slog .NewTextHandler (io .Discard , nil ))
81+ engineInst , err := workflow .NewEngineBuilder ().
82+ WithLogger (engineLogger ).
83+ WithAllDefaults ().
84+ Build ()
85+ if err != nil {
86+ fmt .Fprintf (os .Stderr , "internal error: failed to build engine: %v\n " , err ) //nolint:gosec // G705
87+ os .Exit (1 )
88+ }
89+
90+ // Register all Go command implementations in the CLICommandRegistry service
91+ // before BuildFromConfig so that step.cli_invoke can look them up at
92+ // pipeline execution time (service is resolved lazily on each Execute call).
93+ registry := module .NewCLICommandRegistry ()
94+ for name , fn := range commands {
95+ registry .Register (name , module .CLICommandFunc (fn ))
8896 }
97+ if err := engineInst .App ().RegisterService (module .CLICommandRegistryServiceName , registry ); err != nil {
98+ fmt .Fprintf (os .Stderr , "internal error: failed to register command registry: %v\n " , err ) //nolint:gosec // G705
99+ os .Exit (1 )
100+ }
101+
102+ // Register the CLI-specific step types on the engine's step registry.
103+ // step.cli_invoke calls a Go function by name from CLICommandRegistry.
104+ // step.cli_print writes a template-resolved message to stdout/stderr.
105+ // These are registered here rather than via the pipelinesteps plugin to
106+ // keep wfctl lean — only what the binary actually needs is loaded.
107+ engineInst .AddStepType ("step.cli_invoke" , module .NewCLIInvokeStepFactory ())
108+ engineInst .AddStepType ("step.cli_print" , module .NewCLIPrintStepFactory ())
89109
90- fn , ok := commands [cmd ]
91- if ! ok {
92- fmt .Fprintf (os .Stderr , "unknown command: %s\n \n " , cmd ) //nolint:gosec // G705: CLI error output
93- usage ()
110+ // BuildFromConfig wires the engine from wfctl.yaml:
111+ // 1. CLIWorkflowHandler is configured from workflows.cli (registers itself
112+ // as "cliWorkflowHandler" in the app service registry).
113+ // 2. Each cmd-* pipeline is created and registered.
114+ // 3. CLITrigger is configured once per pipeline (via the "cli" inline
115+ // trigger), accumulating command→pipeline mappings.
116+ if err := engineInst .BuildFromConfig (cfg ); err != nil {
117+ fmt .Fprintf (os .Stderr , "internal error: failed to configure engine: %v\n " , err ) //nolint:gosec // G705
94118 os .Exit (1 )
95119 }
96120
121+ // Retrieve the CLIWorkflowHandler that registered itself during BuildFromConfig.
122+ var cliHandler * handlers.CLIWorkflowHandler
123+ if err := engineInst .App ().GetService (handlers .CLIWorkflowHandlerServiceName , & cliHandler ); err != nil || cliHandler == nil {
124+ fmt .Fprintf (os .Stderr , "internal error: CLIWorkflowHandler not found in service registry\n " ) //nolint:gosec // G705
125+ os .Exit (1 )
126+ }
127+ // Error/usage output goes to stderr; command output goes to stdout.
128+ cliHandler .SetOutput (os .Stderr )
129+
130+ if len (os .Args ) < 2 {
131+ // No subcommand — print usage and exit non-zero.
132+ _ = cliHandler .Dispatch ([]string {"-h" })
133+ os .Exit (1 )
134+ }
135+
136+ cmd := os .Args [1 ]
137+
97138 // Start the update check in the background before running the command so
98139 // that it runs concurrently. For long-running commands (mcp, run) we skip
99140 // it entirely. After the command finishes we wait briefly for the result.
@@ -102,8 +143,20 @@ func main() {
102143 updateNoticeDone = checkForUpdateNotice ()
103144 }
104145
105- if err := fn (os .Args [2 :]); err != nil {
106- fmt .Fprintf (os .Stderr , "error: %v\n " , err ) //nolint:gosec // G705: CLI error output
146+ // Set up a context that is cancelled on SIGINT/SIGTERM so that long-running
147+ // commands (e.g. wfctl mcp, wfctl run) can be interrupted cleanly.
148+ ctx , stop := signal .NotifyContext (context .Background (), syscall .SIGINT , syscall .SIGTERM )
149+
150+ dispatchErr := cliHandler .DispatchContext (ctx , os .Args [1 :])
151+ // Release signal resources before waiting for the update notice or exiting.
152+ stop ()
153+
154+ if dispatchErr != nil {
155+ // The handler already printed routing errors (unknown/missing command).
156+ // Only emit the "error:" prefix for actual command execution failures.
157+ if _ , isKnown := commands [cmd ]; isKnown {
158+ fmt .Fprintf (os .Stderr , "error: %v\n " , dispatchErr ) //nolint:gosec // G705
159+ }
107160 os .Exit (1 )
108161 }
109162
0 commit comments