Skip to content

Commit 9929f8a

Browse files
committed
Merge branch 'feat/workflow-context'
# Conflicts: # cmd/codes/main.go # internal/tui/model.go
2 parents d41c215 + a0b88b0 commit 9929f8a

17 files changed

Lines changed: 1019 additions & 13 deletions

File tree

cmd/codes/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func init() {
3636
rootCmd.AddCommand(commands.ClaudeCmd)
3737
rootCmd.AddCommand(commands.AgentCmd)
3838
rootCmd.AddCommand(commands.TaskSimpleCmd)
39+
rootCmd.AddCommand(commands.WorkflowCmd)
3940

4041
// 设置默认运行时行为
4142
rootCmd.Run = func(cmd *cobra.Command, args []string) {

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ require (
3939
golang.org/x/oauth2 v0.30.0 // indirect
4040
golang.org/x/sys v0.41.0 // indirect
4141
golang.org/x/text v0.3.8 // indirect
42+
gopkg.in/yaml.v3 v3.0.1 // indirect
4243
)

go.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,5 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
8383
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
8484
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
8585
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
86+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
8687
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/commands/cobra.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,12 +287,43 @@ var ProjectScanCmd = &cobra.Command{
287287
},
288288
}
289289

290+
// ProjectLinkCmd links two projects for cross-project context sharing.
291+
var ProjectLinkCmd = &cobra.Command{
292+
Use: "link <project> <linked-project>",
293+
Short: "Link two projects",
294+
Long: "Create a link between two projects for cross-project context sharing",
295+
Args: cobra.ExactArgs(2),
296+
ValidArgsFunction: completeProjectNames,
297+
Run: func(cmd *cobra.Command, args []string) {
298+
role, _ := cmd.Flags().GetString("role")
299+
RunProjectLink(args[0], args[1], role)
300+
},
301+
}
302+
303+
// ProjectUnlinkCmd removes a link between two projects.
304+
var ProjectUnlinkCmd = &cobra.Command{
305+
Use: "unlink <project> <linked-project>",
306+
Short: "Unlink two projects",
307+
Long: "Remove the link between two projects",
308+
Args: cobra.ExactArgs(2),
309+
ValidArgsFunction: completeProjectNames,
310+
Run: func(cmd *cobra.Command, args []string) {
311+
RunProjectUnlink(args[0], args[1])
312+
},
313+
}
314+
315+
func init() {
316+
ProjectLinkCmd.Flags().StringP("role", "r", "", "Role of the linked project (e.g. 'API provider')")
317+
}
318+
290319
func init() {
291320
ProjectAddCmd.Flags().StringP("remote", "r", "", "Remote host name (for remote projects)")
292321
ProjectCmd.AddCommand(ProjectAddCmd)
293322
ProjectCmd.AddCommand(ProjectRemoveCmd)
294323
ProjectCmd.AddCommand(ProjectListCmd)
295324
ProjectCmd.AddCommand(ProjectScanCmd)
325+
ProjectCmd.AddCommand(ProjectLinkCmd)
326+
ProjectCmd.AddCommand(ProjectUnlinkCmd)
296327

297328
ProfileCmd.AddCommand(AddCmd, SelectCmd, TestCmd, ProfileListCmd, ProfileRemoveCmd)
298329

internal/commands/commands.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,6 +1196,28 @@ func RunProjectScan() {
11961196
fmt.Println()
11971197
}
11981198

1199+
// RunProjectLink creates a link between two projects.
1200+
func RunProjectLink(project, linkedProject, role string) {
1201+
if err := config.LinkProject(project, linkedProject, role); err != nil {
1202+
ui.ShowError("Failed to link projects", err)
1203+
return
1204+
}
1205+
msg := fmt.Sprintf("Linked %s → %s", project, linkedProject)
1206+
if role != "" {
1207+
msg += fmt.Sprintf(" (role: %s)", role)
1208+
}
1209+
ui.ShowSuccess("%s", msg)
1210+
}
1211+
1212+
// RunProjectUnlink removes a link between two projects.
1213+
func RunProjectUnlink(project, linkedProject string) {
1214+
if err := config.UnlinkProject(project, linkedProject); err != nil {
1215+
ui.ShowError("Failed to unlink projects", err)
1216+
return
1217+
}
1218+
ui.ShowSuccess("Unlinked %s → %s", project, linkedProject)
1219+
}
1220+
11991221
// runClaudeInDirectory 在指定目录运行 Claude
12001222
func runClaudeInDirectory(dir string) {
12011223
// 调用更新检查
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package commands
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
// WorkflowCmd is the parent command for workflow management.
8+
var WorkflowCmd = &cobra.Command{
9+
Use: "workflow",
10+
Aliases: []string{"wf"},
11+
Short: "Manage and run workflows",
12+
Long: "Create, list, run, and delete reusable workflow templates",
13+
}
14+
15+
var workflowListCmd = &cobra.Command{
16+
Use: "list",
17+
Short: "List all workflows",
18+
Run: func(cmd *cobra.Command, args []string) {
19+
RunWorkflowList()
20+
},
21+
}
22+
23+
var workflowRunCmd = &cobra.Command{
24+
Use: "run <name>",
25+
Short: "Run a workflow",
26+
Args: cobra.ExactArgs(1),
27+
Run: func(cmd *cobra.Command, args []string) {
28+
dir, _ := cmd.Flags().GetString("dir")
29+
model, _ := cmd.Flags().GetString("model")
30+
RunWorkflowRun(args[0], dir, model)
31+
},
32+
}
33+
34+
var workflowCreateCmd = &cobra.Command{
35+
Use: "create <name>",
36+
Short: "Create a new workflow template",
37+
Args: cobra.ExactArgs(1),
38+
Run: func(cmd *cobra.Command, args []string) {
39+
RunWorkflowCreate(args[0])
40+
},
41+
}
42+
43+
var workflowDeleteCmd = &cobra.Command{
44+
Use: "delete <name>",
45+
Short: "Delete a workflow",
46+
Args: cobra.ExactArgs(1),
47+
Run: func(cmd *cobra.Command, args []string) {
48+
RunWorkflowDelete(args[0])
49+
},
50+
}
51+
52+
func init() {
53+
workflowRunCmd.Flags().StringP("dir", "d", "", "Working directory (default: current)")
54+
workflowRunCmd.Flags().StringP("model", "m", "", "Claude model to use")
55+
56+
WorkflowCmd.AddCommand(workflowListCmd)
57+
WorkflowCmd.AddCommand(workflowRunCmd)
58+
WorkflowCmd.AddCommand(workflowCreateCmd)
59+
WorkflowCmd.AddCommand(workflowDeleteCmd)
60+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
8+
"codes/internal/ui"
9+
"codes/internal/workflow"
10+
)
11+
12+
// RunWorkflowList displays all available workflows.
13+
func RunWorkflowList() {
14+
workflows, err := workflow.ListWorkflows()
15+
if err != nil {
16+
ui.ShowError("Failed to list workflows", err)
17+
return
18+
}
19+
20+
if len(workflows) == 0 {
21+
fmt.Println("No workflows found.")
22+
return
23+
}
24+
25+
fmt.Println()
26+
for _, wf := range workflows {
27+
tag := ""
28+
if wf.BuiltIn {
29+
tag = " (built-in)"
30+
}
31+
fmt.Printf(" %s%s\n", wf.Name, tag)
32+
if wf.Description != "" {
33+
fmt.Printf(" %s\n", wf.Description)
34+
}
35+
fmt.Printf(" Steps: %d\n", len(wf.Steps))
36+
fmt.Println()
37+
}
38+
}
39+
40+
// RunWorkflowRun executes a workflow by name.
41+
func RunWorkflowRun(name, dir, model string) {
42+
wf, err := workflow.GetWorkflow(name)
43+
if err != nil {
44+
ui.ShowError("Workflow not found", err)
45+
return
46+
}
47+
48+
if dir == "" {
49+
dir, _ = os.Getwd()
50+
}
51+
52+
fmt.Printf("Running workflow: %s (%d steps)\n", wf.Name, len(wf.Steps))
53+
if wf.Description != "" {
54+
fmt.Printf(" %s\n", wf.Description)
55+
}
56+
fmt.Println()
57+
58+
ctx := context.Background()
59+
run, err := workflow.RunWorkflow(ctx, wf, dir, model)
60+
if err != nil {
61+
ui.ShowError("Workflow execution failed", err)
62+
return
63+
}
64+
65+
// Print results
66+
for i, result := range run.Results {
67+
status := "✓"
68+
if result.Error != "" {
69+
status = "✗"
70+
}
71+
fmt.Printf(" %s Step %d: %s\n", status, i+1, result.StepName)
72+
if result.Error != "" {
73+
fmt.Printf(" Error: %s\n", result.Error)
74+
}
75+
if result.Cost > 0 {
76+
fmt.Printf(" Cost: $%.4f\n", result.Cost)
77+
}
78+
fmt.Println()
79+
}
80+
81+
if run.Status == "completed" {
82+
ui.ShowSuccess("Workflow completed successfully.")
83+
} else {
84+
ui.ShowError(fmt.Sprintf("Workflow %s", run.Status), nil)
85+
}
86+
}
87+
88+
// RunWorkflowCreate creates a new workflow template file.
89+
func RunWorkflowCreate(name string) {
90+
// Check if already exists
91+
if _, err := workflow.GetWorkflow(name); err == nil {
92+
ui.ShowError(fmt.Sprintf("Workflow %q already exists", name), nil)
93+
return
94+
}
95+
96+
wf := &workflow.Workflow{
97+
Name: name,
98+
Description: "Custom workflow",
99+
Steps: []workflow.Step{
100+
{
101+
Name: "Step 1",
102+
Prompt: "Describe what this step should do",
103+
},
104+
},
105+
}
106+
107+
if err := workflow.SaveWorkflow(wf); err != nil {
108+
ui.ShowError("Failed to create workflow", err)
109+
return
110+
}
111+
112+
fmt.Printf("Created workflow template: %s\n", workflow.WorkflowDir()+"/"+name+".yml")
113+
fmt.Println("Edit the YAML file to customize steps.")
114+
}
115+
116+
// RunWorkflowDelete removes a workflow.
117+
func RunWorkflowDelete(name string) {
118+
if err := workflow.DeleteWorkflow(name); err != nil {
119+
ui.ShowError("Failed to delete workflow", err)
120+
return
121+
}
122+
ui.ShowSuccess("Deleted workflow: %s", name)
123+
}

internal/config/config.go

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,9 @@ func (r RemoteHost) UserAtHost() string {
4545

4646
// ProjectEntry represents a project with an optional remote host.
4747
type ProjectEntry struct {
48-
Path string `json:"path"`
49-
Remote string `json:"remote,omitempty"` // remote host name, empty = local
48+
Path string `json:"path"`
49+
Remote string `json:"remote,omitempty"` // remote host name, empty = local
50+
Links []ProjectLink `json:"links,omitempty"` // linked projects
5051
}
5152

5253
// UnmarshalJSON supports both old string format and new object format.
@@ -67,9 +68,9 @@ func (p *ProjectEntry) UnmarshalJSON(data []byte) error {
6768
}
6869

6970
// MarshalJSON saves local projects as plain string (backward compat),
70-
// remote projects as object.
71+
// remote or linked projects as object.
7172
func (p ProjectEntry) MarshalJSON() ([]byte, error) {
72-
if p.Remote == "" {
73+
if p.Remote == "" && len(p.Links) == 0 {
7374
return json.Marshal(p.Path)
7475
}
7576
type Alias ProjectEntry
@@ -665,14 +666,15 @@ func GetDefaultBehavior() string {
665666

666667
// ProjectInfo holds detailed information about a registered project.
667668
type ProjectInfo struct {
668-
Name string `json:"name"`
669-
Path string `json:"path"`
670-
Remote string `json:"remote,omitempty"` // remote host name, empty = local
671-
Exists bool `json:"exists"`
672-
GitBranch string `json:"gitBranch,omitempty"`
673-
GitDirty bool `json:"gitDirty"`
674-
HasClaudeMD bool `json:"hasClaudeMd"`
675-
RecentBranches []string `json:"recentBranches,omitempty"`
669+
Name string `json:"name"`
670+
Path string `json:"path"`
671+
Remote string `json:"remote,omitempty"` // remote host name, empty = local
672+
Exists bool `json:"exists"`
673+
GitBranch string `json:"gitBranch,omitempty"`
674+
GitDirty bool `json:"gitDirty"`
675+
HasClaudeMD bool `json:"hasClaudeMd"`
676+
RecentBranches []string `json:"recentBranches,omitempty"`
677+
Links []ProjectLink `json:"links,omitempty"`
676678
}
677679

678680
// GetProjectInfo aggregates project metadata including git status and file checks.
@@ -686,6 +688,7 @@ func GetProjectInfoFromEntry(name string, entry ProjectEntry) ProjectInfo {
686688
Name: name,
687689
Path: entry.Path,
688690
Remote: entry.Remote,
691+
Links: entry.Links,
689692
}
690693

691694
// For remote projects, skip local filesystem checks

0 commit comments

Comments
 (0)