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,47 @@
|
|
|
1
|
+
package providers
|
|
2
|
+
|
|
3
|
+
import "context"
|
|
4
|
+
|
|
5
|
+
type Role string
|
|
6
|
+
|
|
7
|
+
const (
|
|
8
|
+
RolePlanner Role = "planner"
|
|
9
|
+
RoleCoder Role = "coder"
|
|
10
|
+
RoleReviewer Role = "reviewer"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
type ChatRequest struct {
|
|
14
|
+
Role Role
|
|
15
|
+
Model string
|
|
16
|
+
SystemPrompt string
|
|
17
|
+
UserPrompt string
|
|
18
|
+
MaxTokens int
|
|
19
|
+
Temperature float64
|
|
20
|
+
ReasoningEffort string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Usage struct {
|
|
24
|
+
InputTokens int
|
|
25
|
+
OutputTokens int
|
|
26
|
+
TotalTokens int
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ChatResponse struct {
|
|
30
|
+
Text string
|
|
31
|
+
FinishReason string
|
|
32
|
+
Usage Usage
|
|
33
|
+
ProviderMetadata map[string]string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
type StreamEvent struct {
|
|
37
|
+
Type string
|
|
38
|
+
Text string
|
|
39
|
+
Metadata map[string]string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type Provider interface {
|
|
43
|
+
Name() string
|
|
44
|
+
Validate(ctx context.Context) error
|
|
45
|
+
Chat(ctx context.Context, req ChatRequest) (ChatResponse, error)
|
|
46
|
+
Stream(ctx context.Context, req ChatRequest) (<-chan StreamEvent, <-chan error)
|
|
47
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package providers
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
type Registry struct {
|
|
9
|
+
providers map[string]Provider
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func NewRegistry() *Registry {
|
|
13
|
+
return &Registry{providers: make(map[string]Provider)}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func (r *Registry) Register(p Provider) {
|
|
17
|
+
if r == nil || p == nil {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
r.providers[strings.ToLower(p.Name())] = p
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func (r *Registry) Get(name string) (Provider, error) {
|
|
24
|
+
if r == nil {
|
|
25
|
+
return nil, fmt.Errorf("provider registry is nil")
|
|
26
|
+
}
|
|
27
|
+
p, ok := r.providers[strings.ToLower(strings.TrimSpace(name))]
|
|
28
|
+
if !ok {
|
|
29
|
+
return nil, fmt.Errorf("provider not found: %s", name)
|
|
30
|
+
}
|
|
31
|
+
return p, nil
|
|
32
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package providers
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type fakeProvider struct{ name string }
|
|
11
|
+
|
|
12
|
+
func (f fakeProvider) Name() string { return f.name }
|
|
13
|
+
|
|
14
|
+
func (f fakeProvider) Validate(ctx context.Context) error { return nil }
|
|
15
|
+
|
|
16
|
+
func (f fakeProvider) Chat(ctx context.Context, req ChatRequest) (ChatResponse, error) {
|
|
17
|
+
return ChatResponse{Text: "ok"}, nil
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func (f fakeProvider) Stream(ctx context.Context, req ChatRequest) (<-chan StreamEvent, <-chan error) {
|
|
21
|
+
ev := make(chan StreamEvent)
|
|
22
|
+
err := make(chan error)
|
|
23
|
+
close(ev)
|
|
24
|
+
close(err)
|
|
25
|
+
return ev, err
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func TestRegistryGetProvider(t *testing.T) {
|
|
29
|
+
reg := NewRegistry()
|
|
30
|
+
reg.Register(fakeProvider{name: "openai"})
|
|
31
|
+
|
|
32
|
+
got, err := reg.Get("openai")
|
|
33
|
+
if err != nil {
|
|
34
|
+
t.Fatalf("get provider: %v", err)
|
|
35
|
+
}
|
|
36
|
+
if got.Name() != "openai" {
|
|
37
|
+
t.Fatalf("unexpected provider: %s", got.Name())
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func TestRouterResolveRoleModel(t *testing.T) {
|
|
42
|
+
cfg := config.DefaultConfig()
|
|
43
|
+
reg := NewRegistry()
|
|
44
|
+
reg.Register(fakeProvider{name: "openai"})
|
|
45
|
+
|
|
46
|
+
router := NewRouter(cfg, reg)
|
|
47
|
+
provider, model, err := router.Resolve(RoleCoder)
|
|
48
|
+
if err != nil {
|
|
49
|
+
t.Fatalf("resolve route: %v", err)
|
|
50
|
+
}
|
|
51
|
+
if provider.Name() != "openai" {
|
|
52
|
+
t.Fatalf("unexpected provider: %s", provider.Name())
|
|
53
|
+
}
|
|
54
|
+
if model != cfg.Provider.OpenAI.Models.Coder {
|
|
55
|
+
t.Fatalf("unexpected model: got=%s want=%s", model, cfg.Provider.OpenAI.Models.Coder)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package providers
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
|
|
6
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
type Router struct {
|
|
10
|
+
cfg *config.Config
|
|
11
|
+
registry *Registry
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func NewRouter(cfg *config.Config, registry *Registry) *Router {
|
|
15
|
+
return &Router{cfg: cfg, registry: registry}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func (r *Router) Resolve(role Role) (Provider, string, error) {
|
|
19
|
+
if r == nil || r.cfg == nil || r.registry == nil {
|
|
20
|
+
return nil, "", fmt.Errorf("provider router is not initialized")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
providerName := r.cfg.Provider.Default
|
|
24
|
+
model := modelForRole(r.cfg, role)
|
|
25
|
+
|
|
26
|
+
provider, err := r.registry.Get(providerName)
|
|
27
|
+
if err != nil {
|
|
28
|
+
return nil, "", err
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if model == "" {
|
|
32
|
+
return nil, "", fmt.Errorf("model not configured for role %s", role)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return provider, model, nil
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func modelForRole(cfg *config.Config, role Role) string {
|
|
39
|
+
if cfg == nil {
|
|
40
|
+
return ""
|
|
41
|
+
}
|
|
42
|
+
switch role {
|
|
43
|
+
case RolePlanner:
|
|
44
|
+
return cfg.Provider.OpenAI.Models.Planner
|
|
45
|
+
case RoleCoder:
|
|
46
|
+
return cfg.Provider.OpenAI.Models.Coder
|
|
47
|
+
case RoleReviewer:
|
|
48
|
+
return cfg.Provider.OpenAI.Models.Reviewer
|
|
49
|
+
default:
|
|
50
|
+
return ""
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
package providers
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"sort"
|
|
7
|
+
"strings"
|
|
8
|
+
"time"
|
|
9
|
+
|
|
10
|
+
"github.com/furkanbeydemir/orch/internal/auth"
|
|
11
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
type OpenAIState struct {
|
|
15
|
+
Enabled bool
|
|
16
|
+
Connected bool
|
|
17
|
+
Mode string
|
|
18
|
+
Source string
|
|
19
|
+
Reason string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type ProviderState struct {
|
|
23
|
+
All []string
|
|
24
|
+
Default map[string]string
|
|
25
|
+
Connected []string
|
|
26
|
+
OpenAI OpenAIState
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func ReadState(repoRoot string) (ProviderState, error) {
|
|
30
|
+
cfg, err := config.Load(repoRoot)
|
|
31
|
+
if err != nil {
|
|
32
|
+
return ProviderState{}, fmt.Errorf("failed to load config: %w", err)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
state := ProviderState{
|
|
36
|
+
All: []string{},
|
|
37
|
+
Default: map[string]string{},
|
|
38
|
+
Connected: []string{},
|
|
39
|
+
OpenAI: OpenAIState{
|
|
40
|
+
Enabled: cfg.Provider.Flags.OpenAIEnabled,
|
|
41
|
+
Mode: strings.ToLower(strings.TrimSpace(cfg.Provider.OpenAI.AuthMode)),
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if state.OpenAI.Mode == "" {
|
|
46
|
+
state.OpenAI.Mode = "api_key"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if cfg.Provider.Flags.OpenAIEnabled {
|
|
50
|
+
state.All = append(state.All, "openai")
|
|
51
|
+
state.Default["openai"] = strings.TrimSpace(cfg.Provider.OpenAI.Models.Coder)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if !cfg.Provider.Flags.OpenAIEnabled {
|
|
55
|
+
state.OpenAI.Reason = "provider disabled"
|
|
56
|
+
return state, nil
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
var connected bool
|
|
60
|
+
var source string
|
|
61
|
+
|
|
62
|
+
switch state.OpenAI.Mode {
|
|
63
|
+
case "api_key":
|
|
64
|
+
if strings.TrimSpace(os.Getenv(cfg.Provider.OpenAI.APIKeyEnv)) != "" {
|
|
65
|
+
connected = true
|
|
66
|
+
source = "env"
|
|
67
|
+
} else {
|
|
68
|
+
cred, credErr := auth.Get(repoRoot, "openai")
|
|
69
|
+
if credErr == nil && cred != nil && strings.TrimSpace(strings.ToLower(cred.Type)) == "api" && strings.TrimSpace(cred.Key) != "" {
|
|
70
|
+
connected = true
|
|
71
|
+
source = "local"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if !connected {
|
|
75
|
+
state.OpenAI.Reason = fmt.Sprintf("missing API key (%s or local auth)", cfg.Provider.OpenAI.APIKeyEnv)
|
|
76
|
+
}
|
|
77
|
+
case "account":
|
|
78
|
+
if strings.TrimSpace(os.Getenv(cfg.Provider.OpenAI.AccountTokenEnv)) != "" {
|
|
79
|
+
connected = true
|
|
80
|
+
source = "env"
|
|
81
|
+
} else {
|
|
82
|
+
cred, credErr := auth.Get(repoRoot, "openai")
|
|
83
|
+
if credErr == nil && cred != nil && strings.TrimSpace(strings.ToLower(cred.Type)) == "oauth" && strings.TrimSpace(cred.AccessToken) != "" {
|
|
84
|
+
expired := !cred.ExpiresAt.IsZero() && time.Now().UTC().After(cred.ExpiresAt)
|
|
85
|
+
if expired && strings.TrimSpace(cred.RefreshToken) == "" {
|
|
86
|
+
connected = false
|
|
87
|
+
state.OpenAI.Reason = "stored oauth token expired and missing refresh token"
|
|
88
|
+
} else {
|
|
89
|
+
connected = true
|
|
90
|
+
source = "local"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if !connected {
|
|
95
|
+
if state.OpenAI.Reason == "" {
|
|
96
|
+
state.OpenAI.Reason = fmt.Sprintf("missing account token (%s or local auth)", cfg.Provider.OpenAI.AccountTokenEnv)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
default:
|
|
100
|
+
state.OpenAI.Reason = fmt.Sprintf("invalid auth mode (%s)", state.OpenAI.Mode)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
state.OpenAI.Connected = connected
|
|
104
|
+
state.OpenAI.Source = source
|
|
105
|
+
if connected {
|
|
106
|
+
state.Connected = append(state.Connected, "openai")
|
|
107
|
+
state.OpenAI.Reason = ""
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
sort.Strings(state.All)
|
|
111
|
+
sort.Strings(state.Connected)
|
|
112
|
+
|
|
113
|
+
return state, nil
|
|
114
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
package providers
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
|
|
6
|
+
"github.com/furkanbeydemir/orch/internal/auth"
|
|
7
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestReadStateDisconnectedWithoutCredentials(t *testing.T) {
|
|
11
|
+
repoRoot := t.TempDir()
|
|
12
|
+
cfg := config.DefaultConfig()
|
|
13
|
+
cfg.Provider.Default = "openai"
|
|
14
|
+
cfg.Provider.OpenAI.AuthMode = "api_key"
|
|
15
|
+
if err := config.Save(repoRoot, cfg); err != nil {
|
|
16
|
+
t.Fatalf("save config: %v", err)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
t.Setenv(cfg.Provider.OpenAI.APIKeyEnv, "")
|
|
20
|
+
|
|
21
|
+
state, err := ReadState(repoRoot)
|
|
22
|
+
if err != nil {
|
|
23
|
+
t.Fatalf("read state: %v", err)
|
|
24
|
+
}
|
|
25
|
+
if len(state.All) != 1 || state.All[0] != "openai" {
|
|
26
|
+
t.Fatalf("unexpected all providers: %+v", state.All)
|
|
27
|
+
}
|
|
28
|
+
if state.OpenAI.Connected {
|
|
29
|
+
t.Fatalf("expected disconnected openai state")
|
|
30
|
+
}
|
|
31
|
+
if len(state.Connected) != 0 {
|
|
32
|
+
t.Fatalf("expected no connected providers, got: %+v", state.Connected)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func TestReadStateConnectedWithStoredAPIKey(t *testing.T) {
|
|
37
|
+
repoRoot := t.TempDir()
|
|
38
|
+
cfg := config.DefaultConfig()
|
|
39
|
+
cfg.Provider.Default = "openai"
|
|
40
|
+
cfg.Provider.OpenAI.AuthMode = "api_key"
|
|
41
|
+
if err := config.Save(repoRoot, cfg); err != nil {
|
|
42
|
+
t.Fatalf("save config: %v", err)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if err := auth.Set(repoRoot, "openai", auth.Credential{Type: "api", Key: "sk-test"}); err != nil {
|
|
46
|
+
t.Fatalf("save auth: %v", err)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
t.Setenv(cfg.Provider.OpenAI.APIKeyEnv, "")
|
|
50
|
+
|
|
51
|
+
state, err := ReadState(repoRoot)
|
|
52
|
+
if err != nil {
|
|
53
|
+
t.Fatalf("read state: %v", err)
|
|
54
|
+
}
|
|
55
|
+
if !state.OpenAI.Connected {
|
|
56
|
+
t.Fatalf("expected connected openai state")
|
|
57
|
+
}
|
|
58
|
+
if state.OpenAI.Source != "local" {
|
|
59
|
+
t.Fatalf("expected local source, got: %s", state.OpenAI.Source)
|
|
60
|
+
}
|
|
61
|
+
if len(state.Connected) != 1 || state.Connected[0] != "openai" {
|
|
62
|
+
t.Fatalf("unexpected connected providers: %+v", state.Connected)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// - Primary programming language
|
|
2
|
+
// - File inventory
|
|
3
|
+
package repo
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"encoding/json"
|
|
7
|
+
"fmt"
|
|
8
|
+
"os"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"strings"
|
|
11
|
+
|
|
12
|
+
"github.com/furkanbeydemir/orch/internal/models"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
type Analyzer struct {
|
|
16
|
+
rootPath string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func NewAnalyzer(rootPath string) *Analyzer {
|
|
20
|
+
return &Analyzer{
|
|
21
|
+
rootPath: rootPath,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func (a *Analyzer) Analyze() (*models.RepoMap, error) {
|
|
26
|
+
repoMap := &models.RepoMap{
|
|
27
|
+
RootPath: a.rootPath,
|
|
28
|
+
Files: make([]models.FileInfo, 0),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
err := filepath.Walk(a.rootPath, func(path string, info os.FileInfo, err error) error {
|
|
32
|
+
if err != nil {
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Skip hidden directories and .orch.
|
|
36
|
+
if info.IsDir() {
|
|
37
|
+
name := info.Name()
|
|
38
|
+
if strings.HasPrefix(name, ".") || name == "node_modules" || name == "vendor" {
|
|
39
|
+
return filepath.SkipDir
|
|
40
|
+
}
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Resolve relative path.
|
|
45
|
+
relPath, err := filepath.Rel(a.rootPath, path)
|
|
46
|
+
if err != nil {
|
|
47
|
+
return nil
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fileInfo := models.FileInfo{
|
|
51
|
+
Path: relPath,
|
|
52
|
+
Language: detectLanguage(path),
|
|
53
|
+
Size: info.Size(),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
repoMap.Files = append(repoMap.Files, fileInfo)
|
|
57
|
+
return nil
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if err != nil {
|
|
61
|
+
return nil, fmt.Errorf("repository scan failed: %w", err)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
repoMap.Language = a.detectMainLanguage(repoMap.Files)
|
|
65
|
+
repoMap.PackageManager = a.detectPackageManager()
|
|
66
|
+
repoMap.TestFramework = a.detectTestFramework()
|
|
67
|
+
|
|
68
|
+
// Persist repo map.
|
|
69
|
+
if err := a.saveRepoMap(repoMap); err != nil {
|
|
70
|
+
return nil, err
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return repoMap, nil
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func detectLanguage(path string) string {
|
|
77
|
+
ext := strings.ToLower(filepath.Ext(path))
|
|
78
|
+
languages := map[string]string{
|
|
79
|
+
".go": "go",
|
|
80
|
+
".js": "javascript",
|
|
81
|
+
".ts": "typescript",
|
|
82
|
+
".py": "python",
|
|
83
|
+
".rs": "rust",
|
|
84
|
+
".java": "java",
|
|
85
|
+
".rb": "ruby",
|
|
86
|
+
".cpp": "cpp",
|
|
87
|
+
".c": "c",
|
|
88
|
+
".cs": "csharp",
|
|
89
|
+
".php": "php",
|
|
90
|
+
".swift": "swift",
|
|
91
|
+
".kt": "kotlin",
|
|
92
|
+
".md": "markdown",
|
|
93
|
+
".json": "json",
|
|
94
|
+
".yaml": "yaml",
|
|
95
|
+
".yml": "yaml",
|
|
96
|
+
".toml": "toml",
|
|
97
|
+
".xml": "xml",
|
|
98
|
+
".html": "html",
|
|
99
|
+
".css": "css",
|
|
100
|
+
".sql": "sql",
|
|
101
|
+
".sh": "shell",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if lang, ok := languages[ext]; ok {
|
|
105
|
+
return lang
|
|
106
|
+
}
|
|
107
|
+
return "unknown"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// detectMainLanguage infers the primary language from the file inventory.
|
|
111
|
+
func (a *Analyzer) detectMainLanguage(files []models.FileInfo) string {
|
|
112
|
+
counts := make(map[string]int)
|
|
113
|
+
for _, f := range files {
|
|
114
|
+
if f.Language != "unknown" && f.Language != "markdown" && f.Language != "json" && f.Language != "yaml" {
|
|
115
|
+
counts[f.Language]++
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
maxLang := "unknown"
|
|
120
|
+
maxCount := 0
|
|
121
|
+
for lang, count := range counts {
|
|
122
|
+
if count > maxCount {
|
|
123
|
+
maxLang = lang
|
|
124
|
+
maxCount = count
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return maxLang
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
func (a *Analyzer) detectPackageManager() string {
|
|
132
|
+
indicators := map[string]string{
|
|
133
|
+
"go.mod": "go modules",
|
|
134
|
+
"package.json": "npm",
|
|
135
|
+
"requirements.txt": "pip",
|
|
136
|
+
"Cargo.toml": "cargo",
|
|
137
|
+
"pom.xml": "maven",
|
|
138
|
+
"build.gradle": "gradle",
|
|
139
|
+
"Gemfile": "bundler",
|
|
140
|
+
"composer.json": "composer",
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
for file, manager := range indicators {
|
|
144
|
+
if _, err := os.Stat(filepath.Join(a.rootPath, file)); err == nil {
|
|
145
|
+
return manager
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return "unknown"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
func (a *Analyzer) detectTestFramework() string {
|
|
153
|
+
indicators := map[string]string{
|
|
154
|
+
"go.mod": "go test",
|
|
155
|
+
"jest.config.js": "jest",
|
|
156
|
+
"jest.config.ts": "jest",
|
|
157
|
+
"pytest.ini": "pytest",
|
|
158
|
+
"setup.cfg": "pytest",
|
|
159
|
+
".rspec": "rspec",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for file, framework := range indicators {
|
|
163
|
+
if _, err := os.Stat(filepath.Join(a.rootPath, file)); err == nil {
|
|
164
|
+
return framework
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return "unknown"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
func (a *Analyzer) saveRepoMap(repoMap *models.RepoMap) error {
|
|
172
|
+
orchDir := filepath.Join(a.rootPath, ".orch")
|
|
173
|
+
if err := os.MkdirAll(orchDir, 0o755); err != nil {
|
|
174
|
+
return fmt.Errorf("failed to create .orch directory: %w", err)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
data, err := json.MarshalIndent(repoMap, "", " ")
|
|
178
|
+
if err != nil {
|
|
179
|
+
return fmt.Errorf("failed to serialize repo map: %w", err)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
mapPath := filepath.Join(orchDir, "repo-map.json")
|
|
183
|
+
if err := os.WriteFile(mapPath, data, 0o644); err != nil {
|
|
184
|
+
return fmt.Errorf("failed to write repo map file: %w", err)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return nil
|
|
188
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Package repo - Context Builder implementasyonu.
|
|
2
|
+
//
|
|
3
|
+
// Girdi:
|
|
4
|
+
// - Planner hints (opsiyonel)
|
|
5
|
+
package repo
|
|
6
|
+
|
|
7
|
+
import (
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
|
|
11
|
+
"github.com/furkanbeydemir/orch/internal/models"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
type ContextBuilder struct {
|
|
15
|
+
rootPath string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func NewContextBuilder(rootPath string) *ContextBuilder {
|
|
19
|
+
return &ContextBuilder{
|
|
20
|
+
rootPath: rootPath,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func (cb *ContextBuilder) Build(task *models.Task, repoMap *models.RepoMap, plan *models.Plan) *models.ContextResult {
|
|
25
|
+
result := &models.ContextResult{
|
|
26
|
+
SelectedFiles: make([]string, 0),
|
|
27
|
+
RelatedTests: make([]string, 0),
|
|
28
|
+
RelevantConfigs: make([]string, 0),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if plan != nil {
|
|
32
|
+
result.SelectedFiles = append(result.SelectedFiles, plan.FilesToModify...)
|
|
33
|
+
result.SelectedFiles = append(result.SelectedFiles, plan.FilesToInspect...)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for _, file := range repoMap.Files {
|
|
37
|
+
if isTestFile(file.Path) {
|
|
38
|
+
result.RelatedTests = append(result.RelatedTests, file.Path)
|
|
39
|
+
} else if isConfigFile(file.Path) {
|
|
40
|
+
result.RelevantConfigs = append(result.RelevantConfigs, file.Path)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return result
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func isTestFile(path string) bool {
|
|
48
|
+
base := strings.ToLower(filepath.Base(path))
|
|
49
|
+
patterns := []string{
|
|
50
|
+
"_test.go",
|
|
51
|
+
".test.js", ".test.ts",
|
|
52
|
+
".spec.js", ".spec.ts",
|
|
53
|
+
"test_", "_test.py",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for _, pattern := range patterns {
|
|
57
|
+
if strings.Contains(base, pattern) {
|
|
58
|
+
return true
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
dir := strings.ToLower(filepath.Dir(path))
|
|
63
|
+
return strings.Contains(dir, "test") || strings.Contains(dir, "__tests__")
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
func isConfigFile(path string) bool {
|
|
67
|
+
base := strings.ToLower(filepath.Base(path))
|
|
68
|
+
configs := []string{
|
|
69
|
+
"config", "conf", ".env", ".rc",
|
|
70
|
+
"tsconfig", "jest.config", "webpack",
|
|
71
|
+
"package.json", "go.mod", "cargo.toml",
|
|
72
|
+
"dockerfile", "docker-compose", "makefile",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for _, cfg := range configs {
|
|
76
|
+
if strings.Contains(base, cfg) {
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ext := strings.ToLower(filepath.Ext(path))
|
|
82
|
+
return ext == ".yaml" || ext == ".yml" || ext == ".toml" || ext == ".ini"
|
|
83
|
+
}
|