orch-code 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +624 -0
  4. package/cmd/apply.go +111 -0
  5. package/cmd/auth.go +393 -0
  6. package/cmd/auth_test.go +100 -0
  7. package/cmd/diff.go +57 -0
  8. package/cmd/doctor.go +149 -0
  9. package/cmd/explain.go +192 -0
  10. package/cmd/explain_test.go +62 -0
  11. package/cmd/init.go +100 -0
  12. package/cmd/interactive.go +1372 -0
  13. package/cmd/interactive_input.go +45 -0
  14. package/cmd/interactive_input_test.go +55 -0
  15. package/cmd/logs.go +72 -0
  16. package/cmd/model.go +84 -0
  17. package/cmd/plan.go +149 -0
  18. package/cmd/provider.go +189 -0
  19. package/cmd/provider_model_doctor_test.go +91 -0
  20. package/cmd/root.go +67 -0
  21. package/cmd/run.go +123 -0
  22. package/cmd/run_engine.go +208 -0
  23. package/cmd/run_engine_test.go +30 -0
  24. package/cmd/session.go +589 -0
  25. package/cmd/session_helpers.go +54 -0
  26. package/cmd/session_integration_test.go +30 -0
  27. package/cmd/session_list_current_test.go +87 -0
  28. package/cmd/session_messages_test.go +163 -0
  29. package/cmd/session_runs_test.go +68 -0
  30. package/cmd/sprint1_integration_test.go +119 -0
  31. package/cmd/stats.go +173 -0
  32. package/cmd/stats_test.go +71 -0
  33. package/cmd/version.go +4 -0
  34. package/go.mod +45 -0
  35. package/go.sum +108 -0
  36. package/internal/agents/agent.go +31 -0
  37. package/internal/agents/coder.go +167 -0
  38. package/internal/agents/planner.go +155 -0
  39. package/internal/agents/reviewer.go +118 -0
  40. package/internal/agents/runtime.go +25 -0
  41. package/internal/agents/runtime_test.go +77 -0
  42. package/internal/auth/account.go +78 -0
  43. package/internal/auth/oauth.go +523 -0
  44. package/internal/auth/store.go +287 -0
  45. package/internal/confidence/policy.go +174 -0
  46. package/internal/confidence/policy_test.go +71 -0
  47. package/internal/confidence/scorer.go +253 -0
  48. package/internal/confidence/scorer_test.go +83 -0
  49. package/internal/config/config.go +331 -0
  50. package/internal/config/config_defaults_test.go +138 -0
  51. package/internal/execution/contract_builder.go +160 -0
  52. package/internal/execution/contract_builder_test.go +68 -0
  53. package/internal/execution/plan_compliance.go +161 -0
  54. package/internal/execution/plan_compliance_test.go +71 -0
  55. package/internal/execution/retry_directive.go +132 -0
  56. package/internal/execution/scope_guard.go +69 -0
  57. package/internal/logger/logger.go +120 -0
  58. package/internal/models/contracts_test.go +100 -0
  59. package/internal/models/models.go +269 -0
  60. package/internal/orchestrator/orchestrator.go +701 -0
  61. package/internal/orchestrator/orchestrator_retry_test.go +135 -0
  62. package/internal/orchestrator/review_engine_test.go +50 -0
  63. package/internal/orchestrator/state.go +42 -0
  64. package/internal/orchestrator/test_classifier_test.go +68 -0
  65. package/internal/patch/applier.go +131 -0
  66. package/internal/patch/applier_test.go +25 -0
  67. package/internal/patch/parser.go +89 -0
  68. package/internal/patch/patch.go +60 -0
  69. package/internal/patch/summary.go +30 -0
  70. package/internal/patch/validator.go +104 -0
  71. package/internal/planning/normalizer.go +416 -0
  72. package/internal/planning/normalizer_test.go +64 -0
  73. package/internal/providers/errors.go +35 -0
  74. package/internal/providers/openai/client.go +498 -0
  75. package/internal/providers/openai/client_test.go +187 -0
  76. package/internal/providers/provider.go +47 -0
  77. package/internal/providers/registry.go +32 -0
  78. package/internal/providers/registry_test.go +57 -0
  79. package/internal/providers/router.go +52 -0
  80. package/internal/providers/state.go +114 -0
  81. package/internal/providers/state_test.go +64 -0
  82. package/internal/repo/analyzer.go +188 -0
  83. package/internal/repo/context.go +83 -0
  84. package/internal/review/engine.go +267 -0
  85. package/internal/review/engine_test.go +103 -0
  86. package/internal/runstore/store.go +137 -0
  87. package/internal/runstore/store_test.go +59 -0
  88. package/internal/runtime/lock.go +150 -0
  89. package/internal/runtime/lock_test.go +57 -0
  90. package/internal/session/compaction.go +260 -0
  91. package/internal/session/compaction_test.go +36 -0
  92. package/internal/session/service.go +117 -0
  93. package/internal/session/service_test.go +113 -0
  94. package/internal/storage/storage.go +1498 -0
  95. package/internal/storage/storage_test.go +413 -0
  96. package/internal/testing/classifier.go +80 -0
  97. package/internal/testing/classifier_test.go +36 -0
  98. package/internal/tools/command.go +160 -0
  99. package/internal/tools/command_test.go +56 -0
  100. package/internal/tools/file.go +111 -0
  101. package/internal/tools/git.go +77 -0
  102. package/internal/tools/invalid_params_test.go +36 -0
  103. package/internal/tools/policy.go +98 -0
  104. package/internal/tools/policy_test.go +36 -0
  105. package/internal/tools/registry_test.go +52 -0
  106. package/internal/tools/result.go +30 -0
  107. package/internal/tools/search.go +86 -0
  108. package/internal/tools/tool.go +94 -0
  109. package/main.go +9 -0
  110. package/npm/orch.js +25 -0
  111. package/package.json +41 -0
  112. package/scripts/changelog.js +20 -0
  113. package/scripts/check-release-version.js +21 -0
  114. package/scripts/lib/release-utils.js +223 -0
  115. package/scripts/postinstall.js +157 -0
  116. package/scripts/release.js +52 -0
@@ -0,0 +1,45 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "strings"
6
+ )
7
+
8
+ type interactiveDispatch struct {
9
+ Args []string
10
+ DisplayInput string
11
+ InputNote string
12
+ }
13
+
14
+ func prepareInteractiveDispatch(input string) (interactiveDispatch, error) {
15
+ trimmed := strings.TrimSpace(input)
16
+ if trimmed == "" {
17
+ return interactiveDispatch{}, fmt.Errorf("empty input")
18
+ }
19
+
20
+ if strings.HasPrefix(trimmed, "?quick") {
21
+ payload := strings.TrimSpace(strings.TrimPrefix(trimmed, "?quick"))
22
+ if payload == "" {
23
+ return interactiveDispatch{}, fmt.Errorf("?quick requires a message")
24
+ }
25
+ return interactiveDispatch{
26
+ Args: []string{"chat", buildQuickChatPrompt(payload)},
27
+ DisplayInput: trimmed,
28
+ InputNote: "Local input transform applied: concise chat mode.",
29
+ }, nil
30
+ }
31
+
32
+ args, err := parseInteractiveInput(trimmed)
33
+ if err != nil {
34
+ return interactiveDispatch{}, err
35
+ }
36
+ return interactiveDispatch{
37
+ Args: args,
38
+ DisplayInput: trimmed,
39
+ }, nil
40
+ }
41
+
42
+ func buildQuickChatPrompt(message string) string {
43
+ message = strings.TrimSpace(message)
44
+ return "Respond briefly and practically. Keep the answer tight, actionable, and under 6 lines when possible.\n\nUser request: " + message
45
+ }
@@ -0,0 +1,55 @@
1
+ package cmd
2
+
3
+ import (
4
+ "strings"
5
+ "testing"
6
+ )
7
+
8
+ func TestPrepareInteractiveDispatchPlainChat(t *testing.T) {
9
+ dispatch, err := prepareInteractiveDispatch("selam")
10
+ if err != nil {
11
+ t.Fatalf("prepare dispatch: %v", err)
12
+ }
13
+ if len(dispatch.Args) != 2 || dispatch.Args[0] != "chat" || dispatch.Args[1] != "selam" {
14
+ t.Fatalf("unexpected args: %+v", dispatch.Args)
15
+ }
16
+ if dispatch.DisplayInput != "selam" {
17
+ t.Fatalf("unexpected display input: %q", dispatch.DisplayInput)
18
+ }
19
+ if dispatch.InputNote != "" {
20
+ t.Fatalf("expected no input note, got %q", dispatch.InputNote)
21
+ }
22
+ }
23
+
24
+ func TestPrepareInteractiveDispatchRunCommand(t *testing.T) {
25
+ dispatch, err := prepareInteractiveDispatch("/run fix auth bug")
26
+ if err != nil {
27
+ t.Fatalf("prepare dispatch: %v", err)
28
+ }
29
+ if len(dispatch.Args) != 2 || dispatch.Args[0] != "run" || dispatch.Args[1] != "fix auth bug" {
30
+ t.Fatalf("unexpected args: %+v", dispatch.Args)
31
+ }
32
+ }
33
+
34
+ func TestPrepareInteractiveDispatchQuickTransform(t *testing.T) {
35
+ dispatch, err := prepareInteractiveDispatch("?quick explain confidence policy")
36
+ if err != nil {
37
+ t.Fatalf("prepare dispatch: %v", err)
38
+ }
39
+ if len(dispatch.Args) != 2 || dispatch.Args[0] != "chat" {
40
+ t.Fatalf("unexpected args: %+v", dispatch.Args)
41
+ }
42
+ if !strings.Contains(dispatch.Args[1], "Respond briefly and practically") {
43
+ t.Fatalf("expected transformed prompt, got %q", dispatch.Args[1])
44
+ }
45
+ if dispatch.InputNote == "" {
46
+ t.Fatalf("expected input transform note")
47
+ }
48
+ }
49
+
50
+ func TestPrepareInteractiveDispatchQuickRequiresMessage(t *testing.T) {
51
+ _, err := prepareInteractiveDispatch("?quick")
52
+ if err == nil {
53
+ t.Fatalf("expected error for empty ?quick input")
54
+ }
55
+ }
package/cmd/logs.go ADDED
@@ -0,0 +1,72 @@
1
+ // Package cmd implements the logs command.
2
+ //
3
+ // [analyzer] scanning repository
4
+ // [planner] generating plan
5
+ // [coder] editing userService.ts
6
+ // [test] running npm test
7
+ // [reviewer] patch approved
8
+ package cmd
9
+
10
+ import (
11
+ "fmt"
12
+ "os"
13
+
14
+ "github.com/furkanbeydemir/orch/internal/logger"
15
+ "github.com/spf13/cobra"
16
+ )
17
+
18
+ // logsCmd represents the `orch logs` command.
19
+ var logsCmd = &cobra.Command{
20
+ Use: "logs [run-id]",
21
+ Short: "Shows execution trace",
22
+ Long: `Lists agent execution steps in chronological order.`,
23
+ RunE: runLogs,
24
+ }
25
+
26
+ func init() {
27
+ rootCmd.AddCommand(logsCmd)
28
+ }
29
+
30
+ func runLogs(cmd *cobra.Command, args []string) error {
31
+ cwd, err := os.Getwd()
32
+ if err != nil {
33
+ return fmt.Errorf("failed to get working directory: %w", err)
34
+ }
35
+
36
+ if len(args) > 0 {
37
+ entries, err := logger.LoadRunLog(cwd, args[0])
38
+ if err != nil {
39
+ return fmt.Errorf("failed to load logs: %w", err)
40
+ }
41
+
42
+ for _, entry := range entries {
43
+ fmt.Printf("[%s] %s | %s\n",
44
+ entry.Actor,
45
+ entry.Timestamp.Format("15:04:05"),
46
+ entry.Message,
47
+ )
48
+ }
49
+ return nil
50
+ }
51
+
52
+ runs, err := logger.ListRuns(cwd)
53
+ if err != nil {
54
+ fmt.Println("📋 No run records found yet.")
55
+ fmt.Println(" 'orch run <task>' to run a task.")
56
+ return nil
57
+ }
58
+
59
+ if len(runs) == 0 {
60
+ fmt.Println("📋 No run records found yet.")
61
+ return nil
62
+ }
63
+
64
+ fmt.Println("📋 Run Records:")
65
+ fmt.Println("─────────────────────────────────────")
66
+ for _, run := range runs {
67
+ fmt.Printf(" • %s\n", run)
68
+ }
69
+ fmt.Println("\nFor details: orch logs <run-id>")
70
+
71
+ return nil
72
+ }
package/cmd/model.go ADDED
@@ -0,0 +1,84 @@
1
+ package cmd
2
+
3
+ import (
4
+ "fmt"
5
+ "os"
6
+ "strings"
7
+
8
+ "github.com/furkanbeydemir/orch/internal/config"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ var modelCmd = &cobra.Command{
13
+ Use: "model",
14
+ Short: "Show or manage role model mapping",
15
+ RunE: runModelShow,
16
+ }
17
+
18
+ var modelSetCmd = &cobra.Command{
19
+ Use: "set [role] [model]",
20
+ Short: "Set model for role (planner|coder|reviewer)",
21
+ Args: cobra.ExactArgs(2),
22
+ RunE: runModelSet,
23
+ }
24
+
25
+ func init() {
26
+ modelCmd.AddCommand(modelSetCmd)
27
+ rootCmd.AddCommand(modelCmd)
28
+ }
29
+
30
+ func runModelShow(cmd *cobra.Command, args []string) error {
31
+ cwd, err := os.Getwd()
32
+ if err != nil {
33
+ return fmt.Errorf("failed to get working directory: %w", err)
34
+ }
35
+
36
+ cfg, err := config.Load(cwd)
37
+ if err != nil {
38
+ return fmt.Errorf("failed to load configuration: %w", err)
39
+ }
40
+
41
+ fmt.Println("Model Mapping")
42
+ fmt.Println("-------------")
43
+ fmt.Printf("planner: %s\n", cfg.Provider.OpenAI.Models.Planner)
44
+ fmt.Printf("coder: %s\n", cfg.Provider.OpenAI.Models.Coder)
45
+ fmt.Printf("reviewer: %s\n", cfg.Provider.OpenAI.Models.Reviewer)
46
+
47
+ return nil
48
+ }
49
+
50
+ func runModelSet(cmd *cobra.Command, args []string) error {
51
+ role := strings.ToLower(strings.TrimSpace(args[0]))
52
+ model := strings.TrimSpace(args[1])
53
+ if model == "" {
54
+ return fmt.Errorf("model cannot be empty")
55
+ }
56
+
57
+ cwd, err := os.Getwd()
58
+ if err != nil {
59
+ return fmt.Errorf("failed to get working directory: %w", err)
60
+ }
61
+
62
+ cfg, err := config.Load(cwd)
63
+ if err != nil {
64
+ return fmt.Errorf("failed to load configuration: %w", err)
65
+ }
66
+
67
+ switch role {
68
+ case "planner":
69
+ cfg.Provider.OpenAI.Models.Planner = model
70
+ case "coder":
71
+ cfg.Provider.OpenAI.Models.Coder = model
72
+ case "reviewer":
73
+ cfg.Provider.OpenAI.Models.Reviewer = model
74
+ default:
75
+ return fmt.Errorf("invalid role: %s (expected planner|coder|reviewer)", role)
76
+ }
77
+
78
+ if err := config.Save(cwd, cfg); err != nil {
79
+ return fmt.Errorf("failed to save config: %w", err)
80
+ }
81
+
82
+ fmt.Printf("Model for %s set to %s\n", role, model)
83
+ return nil
84
+ }
package/cmd/plan.go ADDED
@@ -0,0 +1,149 @@
1
+ // Package cmd implements the plan command.
2
+ package cmd
3
+
4
+ import (
5
+ "encoding/json"
6
+ "fmt"
7
+ "os"
8
+ "time"
9
+
10
+ "github.com/furkanbeydemir/orch/internal/config"
11
+ "github.com/furkanbeydemir/orch/internal/models"
12
+ "github.com/furkanbeydemir/orch/internal/orchestrator"
13
+ "github.com/spf13/cobra"
14
+ )
15
+
16
+ var planJSON bool
17
+
18
+ // planCmd represents the `orch plan` command.
19
+ var planCmd = &cobra.Command{
20
+ Use: "plan [task]",
21
+ Short: "Generates an implementation plan for a task",
22
+ Long: `Generates an AI implementation plan for the given task. Does not change code.`,
23
+ Args: cobra.ExactArgs(1),
24
+ RunE: runPlan,
25
+ }
26
+
27
+ func init() {
28
+ planCmd.Flags().BoolVar(&planJSON, "json", false, "Output the structured plan as JSON")
29
+ rootCmd.AddCommand(planCmd)
30
+ }
31
+
32
+ func runPlan(cmd *cobra.Command, args []string) error {
33
+ cwd, err := os.Getwd()
34
+ if err != nil {
35
+ return fmt.Errorf("failed to get working directory: %w", err)
36
+ }
37
+
38
+ cfg, err := config.Load(cwd)
39
+ if err != nil {
40
+ return fmt.Errorf("failed to load configuration: %w", err)
41
+ }
42
+
43
+ task := &models.Task{
44
+ ID: fmt.Sprintf("task-%d", time.Now().UnixNano()),
45
+ Description: args[0],
46
+ CreatedAt: time.Now(),
47
+ }
48
+
49
+ orch := orchestrator.New(cfg, cwd, verbose)
50
+ taskBrief, plan, err := orch.PlanDetailed(task)
51
+ if err != nil {
52
+ return fmt.Errorf("plan generation failed: %w", err)
53
+ }
54
+
55
+ if planJSON {
56
+ payload := struct {
57
+ Task *models.Task `json:"task"`
58
+ TaskBrief *models.TaskBrief `json:"task_brief,omitempty"`
59
+ Plan *models.Plan `json:"plan"`
60
+ }{
61
+ Task: task,
62
+ TaskBrief: taskBrief,
63
+ Plan: plan,
64
+ }
65
+ encoded, marshalErr := json.MarshalIndent(payload, "", " ")
66
+ if marshalErr != nil {
67
+ return fmt.Errorf("failed to encode plan output: %w", marshalErr)
68
+ }
69
+ fmt.Println(string(encoded))
70
+ return nil
71
+ }
72
+
73
+ fmt.Printf("📝 Generating plan: %s\n\n", task.Description)
74
+ fmt.Println("═══════════════════════════════════════")
75
+ fmt.Println("📋 IMPLEMENTATION PLAN")
76
+ fmt.Println("═══════════════════════════════════════")
77
+
78
+ if taskBrief != nil {
79
+ fmt.Printf("\n🎯 Task Type: %s\n", taskBrief.TaskType)
80
+ fmt.Printf("⚠️ Risk Level: %s\n", taskBrief.RiskLevel)
81
+ if taskBrief.NormalizedGoal != "" {
82
+ fmt.Printf("🧭 Goal: %s\n", taskBrief.NormalizedGoal)
83
+ }
84
+ }
85
+
86
+ if plan != nil && plan.Summary != "" {
87
+ fmt.Printf("\n🗺️ Summary: %s\n", plan.Summary)
88
+ }
89
+
90
+ if plan != nil && len(plan.Steps) > 0 {
91
+ fmt.Println("\n📌 Steps:")
92
+ for _, step := range plan.Steps {
93
+ fmt.Printf(" %d. %s\n", step.Order, step.Description)
94
+ }
95
+ }
96
+
97
+ if plan != nil && len(plan.FilesToModify) > 0 {
98
+ fmt.Println("\n📝 Files To Modify:")
99
+ for _, f := range plan.FilesToModify {
100
+ fmt.Printf(" - %s\n", f)
101
+ }
102
+ }
103
+
104
+ if plan != nil && len(plan.FilesToInspect) > 0 {
105
+ fmt.Println("\n🔍 Files To Inspect:")
106
+ for _, f := range plan.FilesToInspect {
107
+ fmt.Printf(" - %s\n", f)
108
+ }
109
+ }
110
+
111
+ if plan != nil && len(plan.AcceptanceCriteria) > 0 {
112
+ fmt.Println("\n✅ Acceptance Criteria:")
113
+ for _, criterion := range plan.AcceptanceCriteria {
114
+ fmt.Printf(" - %s\n", criterion.Description)
115
+ }
116
+ }
117
+
118
+ if plan != nil && len(plan.Invariants) > 0 {
119
+ fmt.Println("\n🧱 Invariants:")
120
+ for _, invariant := range plan.Invariants {
121
+ fmt.Printf(" - %s\n", invariant)
122
+ }
123
+ }
124
+
125
+ if plan != nil && len(plan.ForbiddenChanges) > 0 {
126
+ fmt.Println("\n⛔ Forbidden Changes:")
127
+ for _, forbidden := range plan.ForbiddenChanges {
128
+ fmt.Printf(" - %s\n", forbidden)
129
+ }
130
+ }
131
+
132
+ if plan != nil && len(plan.Risks) > 0 {
133
+ fmt.Println("\n⚠️ Risks:")
134
+ for _, r := range plan.Risks {
135
+ fmt.Printf(" - %s\n", r)
136
+ }
137
+ }
138
+
139
+ if plan != nil && len(plan.TestRequirements) > 0 {
140
+ fmt.Println("\n🧪 Test Requirements:")
141
+ for _, req := range plan.TestRequirements {
142
+ fmt.Printf(" - %s\n", req)
143
+ }
144
+ } else if plan != nil && plan.TestStrategy != "" {
145
+ fmt.Printf("\n🧪 Test Strategy: %s\n", plan.TestStrategy)
146
+ }
147
+
148
+ return nil
149
+ }
@@ -0,0 +1,189 @@
1
+ package cmd
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "os"
7
+ "sort"
8
+ "strings"
9
+
10
+ "github.com/furkanbeydemir/orch/internal/config"
11
+ "github.com/furkanbeydemir/orch/internal/providers"
12
+ "github.com/spf13/cobra"
13
+ )
14
+
15
+ var providerJSONFlag bool
16
+
17
+ var providerCmd = &cobra.Command{
18
+ Use: "provider",
19
+ Short: "Show or manage provider settings",
20
+ RunE: runProviderShow,
21
+ }
22
+
23
+ var providerSetCmd = &cobra.Command{
24
+ Use: "set [provider]",
25
+ Short: "Set default provider",
26
+ Args: cobra.ExactArgs(1),
27
+ RunE: runProviderSet,
28
+ }
29
+
30
+ var providerListCmd = &cobra.Command{
31
+ Use: "list",
32
+ Short: "List all/default/connected providers",
33
+ RunE: runProviderList,
34
+ }
35
+
36
+ func init() {
37
+ providerCmd.Flags().BoolVar(&providerJSONFlag, "json", false, "Output as JSON")
38
+ providerListCmd.Flags().BoolVar(&providerJSONFlag, "json", false, "Output as JSON")
39
+ providerCmd.AddCommand(providerSetCmd)
40
+ providerCmd.AddCommand(providerListCmd)
41
+ rootCmd.AddCommand(providerCmd)
42
+ }
43
+
44
+ func runProviderShow(cmd *cobra.Command, args []string) error {
45
+ cwd, err := os.Getwd()
46
+ if err != nil {
47
+ return fmt.Errorf("failed to get working directory: %w", err)
48
+ }
49
+
50
+ cfg, err := config.Load(cwd)
51
+ if err != nil {
52
+ return fmt.Errorf("failed to load configuration: %w", err)
53
+ }
54
+
55
+ state, err := providers.ReadState(cwd)
56
+ if err != nil {
57
+ return err
58
+ }
59
+
60
+ if providerJSONFlag {
61
+ payload := map[string]any{
62
+ "all": state.All,
63
+ "default": state.Default,
64
+ "connected": state.Connected,
65
+ "openai": map[string]any{
66
+ "enabled": state.OpenAI.Enabled,
67
+ "connected": state.OpenAI.Connected,
68
+ "mode": state.OpenAI.Mode,
69
+ "source": state.OpenAI.Source,
70
+ "reason": state.OpenAI.Reason,
71
+ },
72
+ }
73
+ encoded, marshalErr := json.MarshalIndent(payload, "", " ")
74
+ if marshalErr != nil {
75
+ return fmt.Errorf("failed to serialize provider output: %w", marshalErr)
76
+ }
77
+ fmt.Println(string(encoded))
78
+ return nil
79
+ }
80
+
81
+ fmt.Println("Provider Configuration")
82
+ fmt.Println("----------------------")
83
+ fmt.Printf("default: %s\n", cfg.Provider.Default)
84
+ fmt.Printf("all: %s\n", strings.Join(state.All, ", "))
85
+ if len(state.Connected) == 0 {
86
+ fmt.Println("connected: (none)")
87
+ } else {
88
+ fmt.Printf("connected: %s\n", strings.Join(state.Connected, ", "))
89
+ }
90
+ fmt.Printf("openai.enabled: %t\n", cfg.Provider.Flags.OpenAIEnabled)
91
+ fmt.Printf("openai.authMode: %s\n", cfg.Provider.OpenAI.AuthMode)
92
+ if state.OpenAI.Connected {
93
+ fmt.Printf("openai.connection: connected (%s)\n", state.OpenAI.Source)
94
+ } else {
95
+ fmt.Printf("openai.connection: disconnected (%s)\n", state.OpenAI.Reason)
96
+ }
97
+ fmt.Printf("openai.baseURL: %s\n", cfg.Provider.OpenAI.BaseURL)
98
+ fmt.Printf("openai.apiKeyEnv: %s\n", cfg.Provider.OpenAI.APIKeyEnv)
99
+ fmt.Printf("openai.accountTokenEnv: %s\n", cfg.Provider.OpenAI.AccountTokenEnv)
100
+ fmt.Printf("openai.models: planner=%s coder=%s reviewer=%s\n",
101
+ cfg.Provider.OpenAI.Models.Planner,
102
+ cfg.Provider.OpenAI.Models.Coder,
103
+ cfg.Provider.OpenAI.Models.Reviewer,
104
+ )
105
+
106
+ return nil
107
+ }
108
+
109
+ func runProviderSet(cmd *cobra.Command, args []string) error {
110
+ provider := strings.ToLower(strings.TrimSpace(args[0]))
111
+ if provider != "openai" {
112
+ return fmt.Errorf("unsupported provider: %s (supported: openai)", provider)
113
+ }
114
+
115
+ cwd, err := os.Getwd()
116
+ if err != nil {
117
+ return fmt.Errorf("failed to get working directory: %w", err)
118
+ }
119
+
120
+ cfg, err := config.Load(cwd)
121
+ if err != nil {
122
+ return fmt.Errorf("failed to load configuration: %w", err)
123
+ }
124
+
125
+ cfg.Provider.Default = provider
126
+ cfg.Provider.Flags.OpenAIEnabled = true
127
+
128
+ if err := config.Save(cwd, cfg); err != nil {
129
+ return fmt.Errorf("failed to save config: %w", err)
130
+ }
131
+
132
+ fmt.Printf("Default provider set to %s\n", provider)
133
+ return nil
134
+ }
135
+
136
+ func runProviderList(cmd *cobra.Command, args []string) error {
137
+ cwd, err := os.Getwd()
138
+ if err != nil {
139
+ return fmt.Errorf("failed to get working directory: %w", err)
140
+ }
141
+
142
+ state, err := providers.ReadState(cwd)
143
+ if err != nil {
144
+ return err
145
+ }
146
+
147
+ if providerJSONFlag {
148
+ payload := map[string]any{
149
+ "all": state.All,
150
+ "default": state.Default,
151
+ "connected": state.Connected,
152
+ }
153
+ encoded, marshalErr := json.MarshalIndent(payload, "", " ")
154
+ if marshalErr != nil {
155
+ return fmt.Errorf("failed to serialize provider output: %w", marshalErr)
156
+ }
157
+ fmt.Println(string(encoded))
158
+ return nil
159
+ }
160
+
161
+ fmt.Println("Providers")
162
+ fmt.Println("---------")
163
+ if len(state.All) == 0 {
164
+ fmt.Println("all: (none)")
165
+ } else {
166
+ fmt.Printf("all: %s\n", strings.Join(state.All, ", "))
167
+ }
168
+
169
+ if len(state.Default) == 0 {
170
+ fmt.Println("default: (none)")
171
+ } else {
172
+ keys := make([]string, 0, len(state.Default))
173
+ for provider := range state.Default {
174
+ keys = append(keys, provider)
175
+ }
176
+ sort.Strings(keys)
177
+ for _, provider := range keys {
178
+ fmt.Printf("default.%s: %s\n", provider, state.Default[provider])
179
+ }
180
+ }
181
+
182
+ if len(state.Connected) == 0 {
183
+ fmt.Println("connected: (none)")
184
+ } else {
185
+ fmt.Printf("connected: %s\n", strings.Join(state.Connected, ", "))
186
+ }
187
+
188
+ return nil
189
+ }
@@ -0,0 +1,91 @@
1
+ package cmd
2
+
3
+ import (
4
+ "encoding/json"
5
+ "strings"
6
+ "testing"
7
+
8
+ "github.com/furkanbeydemir/orch/internal/config"
9
+ )
10
+
11
+ func TestProviderAndModelCommands(t *testing.T) {
12
+ repoRoot := t.TempDir()
13
+ t.Chdir(repoRoot)
14
+
15
+ if err := config.EnsureOrchDir(repoRoot); err != nil {
16
+ t.Fatalf("ensure orch dir: %v", err)
17
+ }
18
+ if err := config.Save(repoRoot, config.DefaultConfig()); err != nil {
19
+ t.Fatalf("save config: %v", err)
20
+ }
21
+
22
+ if err := runProviderSet(nil, []string{"openai"}); err != nil {
23
+ t.Fatalf("provider set: %v", err)
24
+ }
25
+
26
+ if err := runModelSet(nil, []string{"coder", "gpt-5.3-codex"}); err != nil {
27
+ t.Fatalf("model set: %v", err)
28
+ }
29
+
30
+ if err := runProviderShow(nil, nil); err != nil {
31
+ t.Fatalf("provider show: %v", err)
32
+ }
33
+ if err := runModelShow(nil, nil); err != nil {
34
+ t.Fatalf("model show: %v", err)
35
+ }
36
+ }
37
+
38
+ func TestDoctorFailsWithoutAPIKey(t *testing.T) {
39
+ repoRoot := t.TempDir()
40
+ t.Chdir(repoRoot)
41
+
42
+ if err := config.EnsureOrchDir(repoRoot); err != nil {
43
+ t.Fatalf("ensure orch dir: %v", err)
44
+ }
45
+ if err := config.Save(repoRoot, config.DefaultConfig()); err != nil {
46
+ t.Fatalf("save config: %v", err)
47
+ }
48
+
49
+ t.Setenv("OPENAI_API_KEY", "")
50
+ err := runDoctor(nil, nil)
51
+ if err == nil {
52
+ t.Fatalf("expected doctor failure without API key")
53
+ }
54
+ if !strings.Contains(err.Error(), "doctor failed") {
55
+ t.Fatalf("unexpected doctor error: %v", err)
56
+ }
57
+ }
58
+
59
+ func TestProviderListJSONOutput(t *testing.T) {
60
+ repoRoot := t.TempDir()
61
+ t.Chdir(repoRoot)
62
+
63
+ if err := config.EnsureOrchDir(repoRoot); err != nil {
64
+ t.Fatalf("ensure orch dir: %v", err)
65
+ }
66
+ if err := config.Save(repoRoot, config.DefaultConfig()); err != nil {
67
+ t.Fatalf("save config: %v", err)
68
+ }
69
+
70
+ providerJSONFlag = true
71
+ defer func() { providerJSONFlag = false }()
72
+
73
+ out := captureStdout(t, func() {
74
+ if err := runProviderList(nil, nil); err != nil {
75
+ t.Fatalf("provider list: %v", err)
76
+ }
77
+ })
78
+
79
+ var payload map[string]any
80
+ if err := json.Unmarshal([]byte(out), &payload); err != nil {
81
+ t.Fatalf("invalid json output: %v\noutput=%s", err, out)
82
+ }
83
+
84
+ all, ok := payload["all"].([]any)
85
+ if !ok || len(all) == 0 {
86
+ t.Fatalf("expected non-empty all providers, got: %#v", payload["all"])
87
+ }
88
+ if all[0] != "openai" {
89
+ t.Fatalf("expected openai in all providers, got: %#v", all)
90
+ }
91
+ }