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
package/cmd/apply.go
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Package cmd implements the apply command.
|
|
2
|
+
package cmd
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"errors"
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
|
|
9
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
10
|
+
"github.com/furkanbeydemir/orch/internal/patch"
|
|
11
|
+
"github.com/spf13/cobra"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
var (
|
|
15
|
+
forceApply bool
|
|
16
|
+
approveDestructive bool
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// applyCmd represents the `orch apply` command.
|
|
20
|
+
var applyCmd = &cobra.Command{
|
|
21
|
+
Use: "apply",
|
|
22
|
+
Short: "Applies generated patch",
|
|
23
|
+
Long: `Applies the generated patch to the working tree.
|
|
24
|
+
Defaults to dry-run mode. Use --force to apply changes.`,
|
|
25
|
+
RunE: runApply,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func init() {
|
|
29
|
+
applyCmd.Flags().BoolVar(&forceApply, "force", false, "Skip dry-run mode and apply changes")
|
|
30
|
+
applyCmd.Flags().BoolVar(&approveDestructive, "approve-destructive", false, "Explicitly approve destructive apply operations")
|
|
31
|
+
rootCmd.AddCommand(applyCmd)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func runApply(cmd *cobra.Command, args []string) error {
|
|
35
|
+
cwd, err := os.Getwd()
|
|
36
|
+
if err != nil {
|
|
37
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
cfg, err := config.Load(cwd)
|
|
41
|
+
if err != nil {
|
|
42
|
+
return fmt.Errorf("failed to load configuration: %w", err)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ctx, err := loadSessionContext(cwd)
|
|
46
|
+
if err != nil {
|
|
47
|
+
return err
|
|
48
|
+
}
|
|
49
|
+
defer ctx.Store.Close()
|
|
50
|
+
|
|
51
|
+
rawDiff, err := ctx.Store.LoadLatestPatchBySession(ctx.Session.ID)
|
|
52
|
+
if err != nil {
|
|
53
|
+
fmt.Println("No patch available to apply yet.")
|
|
54
|
+
fmt.Println("Run 'orch run <task>' first.")
|
|
55
|
+
return nil
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pipeline := patch.NewPipeline(cfg.Patch.MaxFiles, cfg.Patch.MaxLines)
|
|
59
|
+
parsedPatch, err := pipeline.Process(rawDiff)
|
|
60
|
+
if err != nil {
|
|
61
|
+
return fmt.Errorf("invalid patch: %w", err)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
dryRun := cfg.Safety.DryRun
|
|
65
|
+
if forceApply {
|
|
66
|
+
dryRun = false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if cfg.Safety.RequireDestructiveApproval && !dryRun && !approveDestructive {
|
|
70
|
+
return fmt.Errorf("destructive apply blocked: rerun with --approve-destructive")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if dryRun {
|
|
74
|
+
fmt.Println("🔍 Dry-run mode (patch check)...")
|
|
75
|
+
fmt.Println(" For real apply: orch apply --force")
|
|
76
|
+
} else {
|
|
77
|
+
fmt.Println("⚡ Applying patch (force mode)...")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if err := pipeline.Apply(parsedPatch, cwd, dryRun); err != nil {
|
|
81
|
+
var conflictErr *patch.ConflictError
|
|
82
|
+
if errors.As(err, &conflictErr) {
|
|
83
|
+
fmt.Println("❌ Patch conflict detected.")
|
|
84
|
+
for _, file := range conflictErr.Files {
|
|
85
|
+
fmt.Printf(" - %s\n", file)
|
|
86
|
+
}
|
|
87
|
+
if conflictErr.BestPatchSummary != "" {
|
|
88
|
+
fmt.Printf("🩹 Best Patch Summary: %s\n", conflictErr.BestPatchSummary)
|
|
89
|
+
}
|
|
90
|
+
return fmt.Errorf("patch conflict: %s", conflictErr.Reason)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
var invalidPatchErr *patch.InvalidPatchError
|
|
94
|
+
if errors.As(err, &invalidPatchErr) {
|
|
95
|
+
if invalidPatchErr.BestPatchSummary != "" {
|
|
96
|
+
fmt.Printf("🩹 Best Patch Summary: %s\n", invalidPatchErr.BestPatchSummary)
|
|
97
|
+
}
|
|
98
|
+
return fmt.Errorf("invalid diff: %s", invalidPatchErr.Reason)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return fmt.Errorf("patch apply failed: %w", err)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if dryRun {
|
|
105
|
+
fmt.Println("✅ Dry-run succeeded. Patch is applicable.")
|
|
106
|
+
} else {
|
|
107
|
+
fmt.Println("✅ Patch applied successfully.")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return nil
|
|
111
|
+
}
|
package/cmd/auth.go
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"sort"
|
|
8
|
+
"strings"
|
|
9
|
+
"time"
|
|
10
|
+
|
|
11
|
+
"github.com/furkanbeydemir/orch/internal/auth"
|
|
12
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
13
|
+
"github.com/spf13/cobra"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
var (
|
|
17
|
+
authModeFlag string
|
|
18
|
+
authMethodFlag string
|
|
19
|
+
authFlowFlag string
|
|
20
|
+
authProviderFlag string
|
|
21
|
+
authEmailFlag string
|
|
22
|
+
authAPIKeyFlag string
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
var runOAuthLoginFlow = auth.RunOAuthFlow
|
|
26
|
+
|
|
27
|
+
var authCmd = &cobra.Command{
|
|
28
|
+
Use: "auth",
|
|
29
|
+
Short: "Manage provider credentials",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var authLoginCmd = &cobra.Command{
|
|
33
|
+
Use: "login [provider]",
|
|
34
|
+
Short: "Log in to a provider",
|
|
35
|
+
Args: cobra.MaximumNArgs(1),
|
|
36
|
+
RunE: runAuthLogin,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
var authStatusCmd = &cobra.Command{
|
|
40
|
+
Use: "status",
|
|
41
|
+
Short: "Show current authentication status",
|
|
42
|
+
RunE: runAuthStatus,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var authListCmd = &cobra.Command{
|
|
46
|
+
Use: "list",
|
|
47
|
+
Aliases: []string{"ls"},
|
|
48
|
+
Short: "List stored credentials",
|
|
49
|
+
RunE: runAuthList,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var authLogoutCmd = &cobra.Command{
|
|
53
|
+
Use: "logout [provider]",
|
|
54
|
+
Short: "Remove stored credentials",
|
|
55
|
+
Args: cobra.MaximumNArgs(1),
|
|
56
|
+
RunE: runAuthLogout,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
var authOpenAICmd = &cobra.Command{
|
|
60
|
+
Use: "openai",
|
|
61
|
+
Hidden: true,
|
|
62
|
+
Short: "Compatibility shim for old auth syntax",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func init() {
|
|
66
|
+
authLoginCmd.Flags().StringVarP(&authProviderFlag, "provider", "p", "openai", "Provider id")
|
|
67
|
+
authLoginCmd.Flags().StringVarP(&authMethodFlag, "method", "m", "", "Auth method: api or account")
|
|
68
|
+
authLoginCmd.Flags().StringVar(&authFlowFlag, "flow", "auto", "Account auth flow: auto, browser, or headless")
|
|
69
|
+
authLoginCmd.Flags().StringVar(&authModeFlag, "mode", "", "Deprecated: account or api_key")
|
|
70
|
+
authLoginCmd.Flags().StringVar(&authEmailFlag, "email", "", "Account email for status display")
|
|
71
|
+
authLoginCmd.Flags().StringVar(&authAPIKeyFlag, "api-key", "", "API key")
|
|
72
|
+
|
|
73
|
+
authLogoutCmd.Flags().StringVarP(&authProviderFlag, "provider", "p", "openai", "Provider id")
|
|
74
|
+
|
|
75
|
+
authCmd.AddCommand(authLoginCmd)
|
|
76
|
+
authCmd.AddCommand(authStatusCmd)
|
|
77
|
+
authCmd.AddCommand(authListCmd)
|
|
78
|
+
authCmd.AddCommand(authLogoutCmd)
|
|
79
|
+
authOpenAICmd.AddCommand(newAuthCompatLoginCmd())
|
|
80
|
+
authOpenAICmd.AddCommand(newAuthCompatLogoutCmd())
|
|
81
|
+
authOpenAICmd.AddCommand(newAuthCompatStatusCmd())
|
|
82
|
+
authCmd.AddCommand(authOpenAICmd)
|
|
83
|
+
rootCmd.AddCommand(authCmd)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func newAuthCompatLoginCmd() *cobra.Command {
|
|
87
|
+
compat := &cobra.Command{
|
|
88
|
+
Use: "login",
|
|
89
|
+
Short: "Compatibility login command",
|
|
90
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
91
|
+
return runAuthLogin(cmd, []string{"openai"})
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
compat.Flags().StringVarP(&authProviderFlag, "provider", "p", "openai", "Provider id")
|
|
95
|
+
compat.Flags().StringVarP(&authMethodFlag, "method", "m", "", "Auth method: api or account")
|
|
96
|
+
compat.Flags().StringVar(&authFlowFlag, "flow", "auto", "Account auth flow: auto, browser, or headless")
|
|
97
|
+
compat.Flags().StringVar(&authModeFlag, "mode", "", "Deprecated: account or api_key")
|
|
98
|
+
compat.Flags().StringVar(&authEmailFlag, "email", "", "Account email for status display")
|
|
99
|
+
compat.Flags().StringVar(&authAPIKeyFlag, "api-key", "", "API key")
|
|
100
|
+
return compat
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func newAuthCompatLogoutCmd() *cobra.Command {
|
|
104
|
+
compat := &cobra.Command{
|
|
105
|
+
Use: "logout",
|
|
106
|
+
Short: "Compatibility logout command",
|
|
107
|
+
RunE: func(cmd *cobra.Command, args []string) error {
|
|
108
|
+
return runAuthLogout(cmd, []string{"openai"})
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
compat.Flags().StringVarP(&authProviderFlag, "provider", "p", "openai", "Provider id")
|
|
112
|
+
return compat
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
func newAuthCompatStatusCmd() *cobra.Command {
|
|
116
|
+
return &cobra.Command{
|
|
117
|
+
Use: "status",
|
|
118
|
+
Short: "Compatibility status command",
|
|
119
|
+
RunE: runAuthStatus,
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func runAuthLogin(cmd *cobra.Command, args []string) error {
|
|
124
|
+
cwd, err := os.Getwd()
|
|
125
|
+
if err != nil {
|
|
126
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
cfg, err := config.Load(cwd)
|
|
130
|
+
if err != nil {
|
|
131
|
+
return fmt.Errorf("failed to load configuration: %w", err)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
provider := resolveProviderArg(args)
|
|
135
|
+
if provider != "openai" {
|
|
136
|
+
return fmt.Errorf("unsupported provider: %s (supported: openai)", provider)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
method, err := resolveAuthMethod()
|
|
140
|
+
if err != nil {
|
|
141
|
+
return err
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if method == "api" {
|
|
145
|
+
if strings.TrimSpace(authFlowFlag) != "" {
|
|
146
|
+
flow := strings.ToLower(strings.TrimSpace(authFlowFlag))
|
|
147
|
+
if flow != "" && flow != "auto" {
|
|
148
|
+
return fmt.Errorf("--flow is only supported with --method account")
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
key := strings.TrimSpace(authAPIKeyFlag)
|
|
153
|
+
if key == "" {
|
|
154
|
+
fmt.Print("Paste OpenAI API key: ")
|
|
155
|
+
line, readErr := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
156
|
+
if readErr != nil {
|
|
157
|
+
return fmt.Errorf("failed to read API key: %w", readErr)
|
|
158
|
+
}
|
|
159
|
+
key = strings.TrimSpace(line)
|
|
160
|
+
}
|
|
161
|
+
if key == "" {
|
|
162
|
+
return fmt.Errorf("api key cannot be empty")
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if err := auth.Set(cwd, provider, auth.Credential{Type: "api", Key: key}); err != nil {
|
|
166
|
+
return err
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
cfg.Provider.OpenAI.AuthMode = "api_key"
|
|
170
|
+
if err := config.Save(cwd, cfg); err != nil {
|
|
171
|
+
return fmt.Errorf("failed to save config: %w", err)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fmt.Println("Credential saved to .orch/auth.json (0600).")
|
|
175
|
+
fmt.Printf("Auth mode set to api_key. Env %s is still supported with higher priority.\n", cfg.Provider.OpenAI.APIKeyEnv)
|
|
176
|
+
return nil
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
flow, err := resolveAccountFlow()
|
|
180
|
+
if err != nil {
|
|
181
|
+
return err
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fmt.Printf("Starting OpenAI account login (%s)...\n", flow)
|
|
185
|
+
result, err := runOAuthLoginFlow(flow)
|
|
186
|
+
if err != nil {
|
|
187
|
+
return fmt.Errorf("account login failed: %w", err)
|
|
188
|
+
}
|
|
189
|
+
if strings.TrimSpace(result.AccessToken) == "" {
|
|
190
|
+
return fmt.Errorf("oauth flow returned an empty access token")
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
email := strings.TrimSpace(result.Email)
|
|
194
|
+
if explicit := strings.TrimSpace(authEmailFlag); explicit != "" {
|
|
195
|
+
email = explicit
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if err := auth.Set(cwd, provider, auth.Credential{
|
|
199
|
+
Type: "oauth",
|
|
200
|
+
AccessToken: strings.TrimSpace(result.AccessToken),
|
|
201
|
+
RefreshToken: strings.TrimSpace(result.RefreshToken),
|
|
202
|
+
ExpiresAt: result.ExpiresAt,
|
|
203
|
+
AccountID: strings.TrimSpace(result.AccountID),
|
|
204
|
+
Email: email,
|
|
205
|
+
}); err != nil {
|
|
206
|
+
return err
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
cfg.Provider.OpenAI.AuthMode = "account"
|
|
210
|
+
if err := config.Save(cwd, cfg); err != nil {
|
|
211
|
+
return fmt.Errorf("failed to save config: %w", err)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fmt.Println("Credential saved to .orch/auth.json (0600).")
|
|
215
|
+
fmt.Printf("Auth mode set to account. You can also use %s.\n", cfg.Provider.OpenAI.AccountTokenEnv)
|
|
216
|
+
if !result.ExpiresAt.IsZero() {
|
|
217
|
+
fmt.Printf("Token expires at: %s\n", result.ExpiresAt.UTC().Format(time.RFC3339))
|
|
218
|
+
}
|
|
219
|
+
return nil
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
func runAuthStatus(cmd *cobra.Command, args []string) error {
|
|
223
|
+
cwd, err := os.Getwd()
|
|
224
|
+
if err != nil {
|
|
225
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
cfg, err := config.Load(cwd)
|
|
229
|
+
if err != nil {
|
|
230
|
+
return fmt.Errorf("failed to load configuration: %w", err)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
cred, err := auth.Get(cwd, "openai")
|
|
234
|
+
if err != nil {
|
|
235
|
+
return err
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fmt.Println("Auth Status")
|
|
239
|
+
fmt.Println("-----------")
|
|
240
|
+
fmt.Printf("provider: openai\n")
|
|
241
|
+
fmt.Printf("mode: %s\n", cfg.Provider.OpenAI.AuthMode)
|
|
242
|
+
|
|
243
|
+
envAPIKey := strings.TrimSpace(os.Getenv(cfg.Provider.OpenAI.APIKeyEnv)) != ""
|
|
244
|
+
envAccount := strings.TrimSpace(os.Getenv(cfg.Provider.OpenAI.AccountTokenEnv)) != ""
|
|
245
|
+
storedAPIKey := cred != nil && cred.Type == "api" && strings.TrimSpace(cred.Key) != ""
|
|
246
|
+
storedAccount := cred != nil && cred.Type == "oauth" && strings.TrimSpace(cred.AccessToken) != ""
|
|
247
|
+
storedRefresh := cred != nil && cred.Type == "oauth" && strings.TrimSpace(cred.RefreshToken) != ""
|
|
248
|
+
|
|
249
|
+
fmt.Printf("api_key_env: %s (present=%t)\n", cfg.Provider.OpenAI.APIKeyEnv, envAPIKey)
|
|
250
|
+
fmt.Printf("account_token_env: %s (present=%t)\n", cfg.Provider.OpenAI.AccountTokenEnv, envAccount)
|
|
251
|
+
fmt.Printf("stored_api_key: %t\n", storedAPIKey)
|
|
252
|
+
fmt.Printf("stored_account_token: %t\n", storedAccount)
|
|
253
|
+
fmt.Printf("stored_account_refresh: %t\n", storedRefresh)
|
|
254
|
+
if cred != nil && cred.Type == "oauth" && !cred.ExpiresAt.IsZero() {
|
|
255
|
+
fmt.Printf("account_expires_at: %s\n", cred.ExpiresAt.UTC().Format(time.RFC3339))
|
|
256
|
+
}
|
|
257
|
+
if cred != nil && cred.Type == "oauth" && strings.TrimSpace(cred.AccountID) != "" {
|
|
258
|
+
fmt.Printf("account_id: %s\n", cred.AccountID)
|
|
259
|
+
}
|
|
260
|
+
if cred != nil && strings.TrimSpace(cred.Email) != "" {
|
|
261
|
+
fmt.Printf("email: %s\n", cred.Email)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return nil
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
func runAuthList(cmd *cobra.Command, args []string) error {
|
|
268
|
+
cwd, err := os.Getwd()
|
|
269
|
+
if err != nil {
|
|
270
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
all, err := auth.LoadAll(cwd)
|
|
274
|
+
if err != nil {
|
|
275
|
+
return err
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
fmt.Println("Stored Credentials")
|
|
279
|
+
fmt.Println("------------------")
|
|
280
|
+
if len(all) == 0 {
|
|
281
|
+
fmt.Println("No stored credentials found.")
|
|
282
|
+
return nil
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
providers := make([]string, 0, len(all))
|
|
286
|
+
for provider := range all {
|
|
287
|
+
providers = append(providers, provider)
|
|
288
|
+
}
|
|
289
|
+
sort.Strings(providers)
|
|
290
|
+
|
|
291
|
+
for _, provider := range providers {
|
|
292
|
+
cred := all[provider]
|
|
293
|
+
fmt.Printf("%s (%s)\n", provider, cred.Type)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return nil
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
func runAuthLogout(cmd *cobra.Command, args []string) error {
|
|
300
|
+
cwd, err := os.Getwd()
|
|
301
|
+
if err != nil {
|
|
302
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
provider := resolveProviderArg(args)
|
|
306
|
+
if provider != "openai" {
|
|
307
|
+
return fmt.Errorf("unsupported provider: %s (supported: openai)", provider)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if err := auth.Remove(cwd, provider); err != nil {
|
|
311
|
+
return err
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
fmt.Printf("Stored credential removed for %s.\n", provider)
|
|
315
|
+
return nil
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
func resolveProviderArg(args []string) string {
|
|
319
|
+
if len(args) > 0 && strings.TrimSpace(args[0]) != "" {
|
|
320
|
+
return strings.ToLower(strings.TrimSpace(args[0]))
|
|
321
|
+
}
|
|
322
|
+
if strings.TrimSpace(authProviderFlag) != "" {
|
|
323
|
+
return strings.ToLower(strings.TrimSpace(authProviderFlag))
|
|
324
|
+
}
|
|
325
|
+
return "openai"
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func resolveAuthMethod() (string, error) {
|
|
329
|
+
mode := strings.ToLower(strings.TrimSpace(authModeFlag))
|
|
330
|
+
if mode != "" {
|
|
331
|
+
if mode == "api_key" {
|
|
332
|
+
return "api", nil
|
|
333
|
+
}
|
|
334
|
+
if mode == "account" {
|
|
335
|
+
return "account", nil
|
|
336
|
+
}
|
|
337
|
+
return "", fmt.Errorf("invalid auth mode: %s (expected account or api_key)", mode)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
method := strings.ToLower(strings.TrimSpace(authMethodFlag))
|
|
341
|
+
if method == "api_key" || method == "key" {
|
|
342
|
+
method = "api"
|
|
343
|
+
}
|
|
344
|
+
if method == "oauth" || method == "browser" || method == "headless" {
|
|
345
|
+
method = "account"
|
|
346
|
+
}
|
|
347
|
+
if method == "api" || method == "account" {
|
|
348
|
+
return method, nil
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if strings.TrimSpace(authAPIKeyFlag) != "" {
|
|
352
|
+
return "api", nil
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
fmt.Println("Select auth method:")
|
|
356
|
+
fmt.Println(" 1) OpenAI account (browser)")
|
|
357
|
+
fmt.Println(" 2) API key")
|
|
358
|
+
fmt.Print("Choice [1-2]: ")
|
|
359
|
+
line, err := bufio.NewReader(os.Stdin).ReadString('\n')
|
|
360
|
+
if err != nil {
|
|
361
|
+
return "", fmt.Errorf("failed to read choice: %w", err)
|
|
362
|
+
}
|
|
363
|
+
choice := strings.TrimSpace(line)
|
|
364
|
+
if choice == "" || choice == "1" {
|
|
365
|
+
return "account", nil
|
|
366
|
+
}
|
|
367
|
+
if choice == "2" {
|
|
368
|
+
return "api", nil
|
|
369
|
+
}
|
|
370
|
+
return "", fmt.Errorf("invalid auth choice: %s", choice)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
func resolveAccountFlow() (string, error) {
|
|
374
|
+
flow := strings.ToLower(strings.TrimSpace(authFlowFlag))
|
|
375
|
+
if flow == "" {
|
|
376
|
+
flow = "auto"
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if flow == "auto" {
|
|
380
|
+
methodHint := strings.ToLower(strings.TrimSpace(authMethodFlag))
|
|
381
|
+
switch methodHint {
|
|
382
|
+
case "browser", "headless":
|
|
383
|
+
flow = methodHint
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
switch flow {
|
|
388
|
+
case "auto", "browser", "headless":
|
|
389
|
+
return flow, nil
|
|
390
|
+
default:
|
|
391
|
+
return "", fmt.Errorf("invalid account flow: %s (expected auto, browser, or headless)", flow)
|
|
392
|
+
}
|
|
393
|
+
}
|
package/cmd/auth_test.go
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"testing"
|
|
5
|
+
"time"
|
|
6
|
+
|
|
7
|
+
"github.com/furkanbeydemir/orch/internal/auth"
|
|
8
|
+
"github.com/furkanbeydemir/orch/internal/config"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func TestAuthLoginAccountAndLogout(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
|
+
originalOAuthRunner := runOAuthLoginFlow
|
|
23
|
+
runOAuthLoginFlow = func(flow string) (auth.OAuthResult, error) {
|
|
24
|
+
return auth.OAuthResult{
|
|
25
|
+
AccessToken: "token-123",
|
|
26
|
+
RefreshToken: "refresh-123",
|
|
27
|
+
ExpiresAt: time.Now().UTC().Add(1 * time.Hour),
|
|
28
|
+
AccountID: "acc-123",
|
|
29
|
+
Email: "oauth@example.com",
|
|
30
|
+
}, nil
|
|
31
|
+
}
|
|
32
|
+
defer func() {
|
|
33
|
+
runOAuthLoginFlow = originalOAuthRunner
|
|
34
|
+
}()
|
|
35
|
+
|
|
36
|
+
authModeFlag = "account"
|
|
37
|
+
authMethodFlag = ""
|
|
38
|
+
authFlowFlag = "headless"
|
|
39
|
+
authProviderFlag = "openai"
|
|
40
|
+
authEmailFlag = "user@example.com"
|
|
41
|
+
authAPIKeyFlag = ""
|
|
42
|
+
if err := runAuthLogin(nil, nil); err != nil {
|
|
43
|
+
t.Fatalf("auth login account: %v", err)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
state, err := auth.Load(repoRoot)
|
|
47
|
+
if err != nil {
|
|
48
|
+
t.Fatalf("load auth state: %v", err)
|
|
49
|
+
}
|
|
50
|
+
if state == nil || state.AccessToken != "token-123" {
|
|
51
|
+
t.Fatalf("expected stored account token")
|
|
52
|
+
}
|
|
53
|
+
if state.RefreshToken != "refresh-123" {
|
|
54
|
+
t.Fatalf("expected stored refresh token")
|
|
55
|
+
}
|
|
56
|
+
if state.AccountID != "acc-123" {
|
|
57
|
+
t.Fatalf("expected stored account id")
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if err := runAuthLogout(nil, nil); err != nil {
|
|
61
|
+
t.Fatalf("auth logout: %v", err)
|
|
62
|
+
}
|
|
63
|
+
state, err = auth.Load(repoRoot)
|
|
64
|
+
if err != nil {
|
|
65
|
+
t.Fatalf("load auth state after logout: %v", err)
|
|
66
|
+
}
|
|
67
|
+
if state != nil {
|
|
68
|
+
t.Fatalf("expected auth state to be removed")
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func TestAuthLoginAPIKeyMode(t *testing.T) {
|
|
73
|
+
repoRoot := t.TempDir()
|
|
74
|
+
t.Chdir(repoRoot)
|
|
75
|
+
|
|
76
|
+
if err := config.EnsureOrchDir(repoRoot); err != nil {
|
|
77
|
+
t.Fatalf("ensure orch dir: %v", err)
|
|
78
|
+
}
|
|
79
|
+
if err := config.Save(repoRoot, config.DefaultConfig()); err != nil {
|
|
80
|
+
t.Fatalf("save config: %v", err)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
authModeFlag = ""
|
|
84
|
+
authMethodFlag = "api"
|
|
85
|
+
authFlowFlag = ""
|
|
86
|
+
authProviderFlag = "openai"
|
|
87
|
+
authEmailFlag = ""
|
|
88
|
+
authAPIKeyFlag = "sk-test"
|
|
89
|
+
if err := runAuthLogin(nil, nil); err != nil {
|
|
90
|
+
t.Fatalf("auth login api_key: %v", err)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
cfg, err := config.Load(repoRoot)
|
|
94
|
+
if err != nil {
|
|
95
|
+
t.Fatalf("load config: %v", err)
|
|
96
|
+
}
|
|
97
|
+
if cfg.Provider.OpenAI.AuthMode != "api_key" {
|
|
98
|
+
t.Fatalf("expected auth mode api_key, got %s", cfg.Provider.OpenAI.AuthMode)
|
|
99
|
+
}
|
|
100
|
+
}
|
package/cmd/diff.go
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Package cmd implements the diff command.
|
|
2
|
+
package cmd
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
|
|
8
|
+
"github.com/furkanbeydemir/orch/internal/patch"
|
|
9
|
+
"github.com/spf13/cobra"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// diffCmd represents the `orch diff` command.
|
|
13
|
+
var diffCmd = &cobra.Command{
|
|
14
|
+
Use: "diff",
|
|
15
|
+
Short: "Shows the generated patch",
|
|
16
|
+
Long: `Shows the unified diff patch from the latest run.`,
|
|
17
|
+
RunE: runDiff,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func init() {
|
|
21
|
+
rootCmd.AddCommand(diffCmd)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func runDiff(cmd *cobra.Command, args []string) error {
|
|
25
|
+
cwd, err := os.Getwd()
|
|
26
|
+
if err != nil {
|
|
27
|
+
return fmt.Errorf("failed to get working directory: %w", err)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
ctx, err := loadSessionContext(cwd)
|
|
31
|
+
if err != nil {
|
|
32
|
+
return err
|
|
33
|
+
}
|
|
34
|
+
defer ctx.Store.Close()
|
|
35
|
+
|
|
36
|
+
rawDiff, err := ctx.Store.LoadLatestPatchBySession(ctx.Session.ID)
|
|
37
|
+
if err != nil {
|
|
38
|
+
fmt.Println("📄 Latest generated patch:")
|
|
39
|
+
fmt.Println("─────────────────────────────────────")
|
|
40
|
+
fmt.Println("No generated patch found yet.")
|
|
41
|
+
fmt.Println("Run 'orch run <task>' first.")
|
|
42
|
+
return nil
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
p := patch.NewPipeline(10, 800)
|
|
46
|
+
parsed, err := p.Process(rawDiff)
|
|
47
|
+
if err != nil {
|
|
48
|
+
return fmt.Errorf("patch parse/validation error: %w", err)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fmt.Println("📄 Latest generated patch:")
|
|
52
|
+
fmt.Println("─────────────────────────────────────")
|
|
53
|
+
fmt.Printf("Files: %d | Line limit: <= 800\n", len(parsed.Files))
|
|
54
|
+
fmt.Println()
|
|
55
|
+
fmt.Print(rawDiff)
|
|
56
|
+
return nil
|
|
57
|
+
}
|