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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +624 -0
- package/cmd/apply.go +111 -0
- package/cmd/auth.go +393 -0
- package/cmd/auth_test.go +100 -0
- package/cmd/diff.go +57 -0
- package/cmd/doctor.go +149 -0
- package/cmd/explain.go +192 -0
- package/cmd/explain_test.go +62 -0
- package/cmd/init.go +100 -0
- package/cmd/interactive.go +1372 -0
- package/cmd/interactive_input.go +45 -0
- package/cmd/interactive_input_test.go +55 -0
- package/cmd/logs.go +72 -0
- package/cmd/model.go +84 -0
- package/cmd/plan.go +149 -0
- package/cmd/provider.go +189 -0
- package/cmd/provider_model_doctor_test.go +91 -0
- package/cmd/root.go +67 -0
- package/cmd/run.go +123 -0
- package/cmd/run_engine.go +208 -0
- package/cmd/run_engine_test.go +30 -0
- package/cmd/session.go +589 -0
- package/cmd/session_helpers.go +54 -0
- package/cmd/session_integration_test.go +30 -0
- package/cmd/session_list_current_test.go +87 -0
- package/cmd/session_messages_test.go +163 -0
- package/cmd/session_runs_test.go +68 -0
- package/cmd/sprint1_integration_test.go +119 -0
- package/cmd/stats.go +173 -0
- package/cmd/stats_test.go +71 -0
- package/cmd/version.go +4 -0
- package/go.mod +45 -0
- package/go.sum +108 -0
- package/internal/agents/agent.go +31 -0
- package/internal/agents/coder.go +167 -0
- package/internal/agents/planner.go +155 -0
- package/internal/agents/reviewer.go +118 -0
- package/internal/agents/runtime.go +25 -0
- package/internal/agents/runtime_test.go +77 -0
- package/internal/auth/account.go +78 -0
- package/internal/auth/oauth.go +523 -0
- package/internal/auth/store.go +287 -0
- package/internal/confidence/policy.go +174 -0
- package/internal/confidence/policy_test.go +71 -0
- package/internal/confidence/scorer.go +253 -0
- package/internal/confidence/scorer_test.go +83 -0
- package/internal/config/config.go +331 -0
- package/internal/config/config_defaults_test.go +138 -0
- package/internal/execution/contract_builder.go +160 -0
- package/internal/execution/contract_builder_test.go +68 -0
- package/internal/execution/plan_compliance.go +161 -0
- package/internal/execution/plan_compliance_test.go +71 -0
- package/internal/execution/retry_directive.go +132 -0
- package/internal/execution/scope_guard.go +69 -0
- package/internal/logger/logger.go +120 -0
- package/internal/models/contracts_test.go +100 -0
- package/internal/models/models.go +269 -0
- package/internal/orchestrator/orchestrator.go +701 -0
- package/internal/orchestrator/orchestrator_retry_test.go +135 -0
- package/internal/orchestrator/review_engine_test.go +50 -0
- package/internal/orchestrator/state.go +42 -0
- package/internal/orchestrator/test_classifier_test.go +68 -0
- package/internal/patch/applier.go +131 -0
- package/internal/patch/applier_test.go +25 -0
- package/internal/patch/parser.go +89 -0
- package/internal/patch/patch.go +60 -0
- package/internal/patch/summary.go +30 -0
- package/internal/patch/validator.go +104 -0
- package/internal/planning/normalizer.go +416 -0
- package/internal/planning/normalizer_test.go +64 -0
- package/internal/providers/errors.go +35 -0
- package/internal/providers/openai/client.go +498 -0
- package/internal/providers/openai/client_test.go +187 -0
- package/internal/providers/provider.go +47 -0
- package/internal/providers/registry.go +32 -0
- package/internal/providers/registry_test.go +57 -0
- package/internal/providers/router.go +52 -0
- package/internal/providers/state.go +114 -0
- package/internal/providers/state_test.go +64 -0
- package/internal/repo/analyzer.go +188 -0
- package/internal/repo/context.go +83 -0
- package/internal/review/engine.go +267 -0
- package/internal/review/engine_test.go +103 -0
- package/internal/runstore/store.go +137 -0
- package/internal/runstore/store_test.go +59 -0
- package/internal/runtime/lock.go +150 -0
- package/internal/runtime/lock_test.go +57 -0
- package/internal/session/compaction.go +260 -0
- package/internal/session/compaction_test.go +36 -0
- package/internal/session/service.go +117 -0
- package/internal/session/service_test.go +113 -0
- package/internal/storage/storage.go +1498 -0
- package/internal/storage/storage_test.go +413 -0
- package/internal/testing/classifier.go +80 -0
- package/internal/testing/classifier_test.go +36 -0
- package/internal/tools/command.go +160 -0
- package/internal/tools/command_test.go +56 -0
- package/internal/tools/file.go +111 -0
- package/internal/tools/git.go +77 -0
- package/internal/tools/invalid_params_test.go +36 -0
- package/internal/tools/policy.go +98 -0
- package/internal/tools/policy_test.go +36 -0
- package/internal/tools/registry_test.go +52 -0
- package/internal/tools/result.go +30 -0
- package/internal/tools/search.go +86 -0
- package/internal/tools/tool.go +94 -0
- package/main.go +9 -0
- package/npm/orch.js +25 -0
- package/package.json +41 -0
- package/scripts/changelog.js +20 -0
- package/scripts/check-release-version.js +21 -0
- package/scripts/lib/release-utils.js +223 -0
- package/scripts/postinstall.js +157 -0
- 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
|
+
}
|
package/cmd/provider.go
ADDED
|
@@ -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
|
+
}
|