@dcode-dev/dcode-cli 1.0.0
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/NPM_README.md +78 -0
- package/README.md +341 -0
- package/bin/dcode-bin +0 -0
- package/bin/dcode.js +44 -0
- package/cmd/agent_v2.go +448 -0
- package/cmd/analyze.go +97 -0
- package/cmd/auth.go +338 -0
- package/cmd/compose.go +284 -0
- package/cmd/context.go +111 -0
- package/cmd/edit.go +116 -0
- package/cmd/env.go +10 -0
- package/cmd/fix.go +145 -0
- package/cmd/gemini.go +20 -0
- package/cmd/generate.go +47 -0
- package/cmd/interactive.go +33 -0
- package/cmd/mcp.go +196 -0
- package/cmd/patch.go +19 -0
- package/cmd/providers.go +67 -0
- package/cmd/root.go +41 -0
- package/cmd/search.go +61 -0
- package/cmd/server.go +36 -0
- package/cmd/switch.go +122 -0
- package/cmd/terminal.go +277 -0
- package/go.mod +42 -0
- package/go.sum +86 -0
- package/internal/agent/agent.go +332 -0
- package/internal/agent/parse.go +25 -0
- package/internal/agents/base.go +154 -0
- package/internal/agents/documenter.go +77 -0
- package/internal/agents/generalist.go +266 -0
- package/internal/agents/investigator.go +60 -0
- package/internal/agents/registry.go +34 -0
- package/internal/agents/reviewer.go +67 -0
- package/internal/agents/tester.go +73 -0
- package/internal/ai/client.go +634 -0
- package/internal/ai/tools.go +332 -0
- package/internal/auth/adc.go +108 -0
- package/internal/auth/apikey.go +67 -0
- package/internal/auth/factory.go +145 -0
- package/internal/auth/oauth2.go +227 -0
- package/internal/auth/store.go +216 -0
- package/internal/auth/types.go +79 -0
- package/internal/auth/vertex.go +138 -0
- package/internal/config/config.go +428 -0
- package/internal/config/policy.go +251 -0
- package/internal/context/builder.go +312 -0
- package/internal/detector/detector.go +204 -0
- package/internal/diffutil/diffutil.go +30 -0
- package/internal/fsutil/fsutil.go +35 -0
- package/internal/mcp/client.go +314 -0
- package/internal/mcp/manager.go +221 -0
- package/internal/policy/policy.go +89 -0
- package/internal/prompt/interactive.go +338 -0
- package/internal/registry/agent.go +201 -0
- package/internal/registry/tool.go +181 -0
- package/internal/scheduler/scheduler.go +250 -0
- package/internal/server/server.go +167 -0
- package/internal/tools/file.go +183 -0
- package/internal/tools/filesystem.go +286 -0
- package/internal/tools/git.go +355 -0
- package/internal/tools/memory.go +269 -0
- package/internal/tools/registry.go +49 -0
- package/internal/tools/search.go +230 -0
- package/internal/tools/shell.go +84 -0
- package/internal/websearch/search.go +40 -0
- package/internal/websearch/tavily.go +79 -0
- package/main.go +19 -0
- package/package.json +57 -0
- package/scripts/install.js +59 -0
- package/scripts/uninstall.js +28 -0
package/cmd/auth.go
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"time"
|
|
8
|
+
|
|
9
|
+
"github.com/ddhanush1/dcode/internal/auth"
|
|
10
|
+
"github.com/ddhanush1/dcode/internal/config"
|
|
11
|
+
"github.com/spf13/cobra"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
var authCmd = &cobra.Command{
|
|
15
|
+
Use: "auth",
|
|
16
|
+
Short: "Authentication management",
|
|
17
|
+
Long: `Manage authentication credentials for various AI providers`,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var loginCmd = &cobra.Command{
|
|
21
|
+
Use: "login",
|
|
22
|
+
Short: "Login to an AI provider",
|
|
23
|
+
Long: `Authenticate with an AI provider using OAuth2 flow`,
|
|
24
|
+
RunE: runLogin,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
var logoutCmd = &cobra.Command{
|
|
28
|
+
Use: "logout",
|
|
29
|
+
Short: "Logout from an AI provider",
|
|
30
|
+
Long: `Remove stored credentials for an AI provider`,
|
|
31
|
+
RunE: runLogout,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var statusCmd = &cobra.Command{
|
|
35
|
+
Use: "status",
|
|
36
|
+
Short: "Show authentication status",
|
|
37
|
+
Long: `Display current authentication status for all providers`,
|
|
38
|
+
RunE: runStatus,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func init() {
|
|
42
|
+
rootCmd.AddCommand(authCmd)
|
|
43
|
+
authCmd.AddCommand(loginCmd)
|
|
44
|
+
authCmd.AddCommand(logoutCmd)
|
|
45
|
+
authCmd.AddCommand(statusCmd)
|
|
46
|
+
|
|
47
|
+
loginCmd.Flags().String("provider", "gemini", "AI provider (openai, gemini, claude)")
|
|
48
|
+
loginCmd.Flags().String("type", "oauth2", "Auth type (oauth2, adc, api_key)")
|
|
49
|
+
|
|
50
|
+
logoutCmd.Flags().String("provider", "", "AI provider to logout from (default: all)")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func runLogin(cmd *cobra.Command, args []string) error {
|
|
54
|
+
provider, _ := cmd.Flags().GetString("provider")
|
|
55
|
+
authType, _ := cmd.Flags().GetString("type")
|
|
56
|
+
|
|
57
|
+
fmt.Printf("🔐 Logging in to %s using %s...\n\n", provider, authType)
|
|
58
|
+
|
|
59
|
+
store := auth.GetDefaultStore()
|
|
60
|
+
|
|
61
|
+
cfg, err := config.LoadConfig()
|
|
62
|
+
if err != nil {
|
|
63
|
+
return fmt.Errorf("failed to load config: %w", err)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Override with flags
|
|
67
|
+
cfg.Auth.Provider = provider
|
|
68
|
+
cfg.Auth.Type = authType
|
|
69
|
+
|
|
70
|
+
ctx := context.Background()
|
|
71
|
+
|
|
72
|
+
switch auth.AuthType(authType) {
|
|
73
|
+
case auth.AuthTypeOAuth2:
|
|
74
|
+
return runOAuth2Login(ctx, provider, store)
|
|
75
|
+
|
|
76
|
+
case auth.AuthTypeADC:
|
|
77
|
+
return runADCLogin(ctx, provider)
|
|
78
|
+
|
|
79
|
+
case auth.AuthTypeAPIKey:
|
|
80
|
+
return runAPIKeyLogin(provider, store)
|
|
81
|
+
|
|
82
|
+
default:
|
|
83
|
+
return fmt.Errorf("unsupported auth type: %s", authType)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func runOAuth2Login(ctx context.Context, provider string, store auth.CredentialStore) error {
|
|
88
|
+
clientID := os.Getenv("GOOGLE_CLIENT_ID")
|
|
89
|
+
clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
|
|
90
|
+
|
|
91
|
+
if clientID == "" || clientSecret == "" {
|
|
92
|
+
fmt.Println("❌ OAuth2 requires GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET environment variables")
|
|
93
|
+
fmt.Println("\nTo set up OAuth2:")
|
|
94
|
+
fmt.Println("1. Go to https://console.cloud.google.com/apis/credentials")
|
|
95
|
+
fmt.Println("2. Create OAuth 2.0 Client ID")
|
|
96
|
+
fmt.Println("3. Set redirect URI to: http://localhost:8085/callback")
|
|
97
|
+
fmt.Println("4. Export credentials:")
|
|
98
|
+
fmt.Println(" export GOOGLE_CLIENT_ID=your-client-id")
|
|
99
|
+
fmt.Println(" export GOOGLE_CLIENT_SECRET=your-client-secret")
|
|
100
|
+
return fmt.Errorf("OAuth2 credentials not configured")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
scopes := []string{
|
|
104
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
105
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
106
|
+
"https://www.googleapis.com/auth/generative-language",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
authenticator := auth.NewOAuth2Auth(provider, clientID, clientSecret, scopes, store)
|
|
110
|
+
|
|
111
|
+
// Start auth flow
|
|
112
|
+
authURL, err := authenticator.StartAuthFlow(ctx)
|
|
113
|
+
if err != nil {
|
|
114
|
+
return fmt.Errorf("failed to start auth flow: %w", err)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fmt.Println("📱 Opening browser for authentication...")
|
|
118
|
+
fmt.Printf("\nIf browser doesn't open, visit:\n%s\n\n", authURL)
|
|
119
|
+
|
|
120
|
+
// TODO: Implement OAuth callback server
|
|
121
|
+
fmt.Println("⚠️ OAuth2 callback server not yet implemented")
|
|
122
|
+
fmt.Println("For now, use API key authentication with: dcode auth login --type api_key")
|
|
123
|
+
|
|
124
|
+
return nil
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
func runADCLogin(ctx context.Context, provider string) error {
|
|
128
|
+
fmt.Println("🔍 Checking for Application Default Credentials...")
|
|
129
|
+
|
|
130
|
+
if !auth.IsADCAvailable(ctx) {
|
|
131
|
+
fmt.Println("❌ ADC not available")
|
|
132
|
+
fmt.Println("\nTo set up ADC:")
|
|
133
|
+
fmt.Println("1. Install gcloud CLI: https://cloud.google.com/sdk/docs/install")
|
|
134
|
+
fmt.Println("2. Run: gcloud auth application-default login")
|
|
135
|
+
return fmt.Errorf("ADC not configured")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
authenticator := auth.NewADCAuth(provider, []string{"https://www.googleapis.com/auth/cloud-platform"})
|
|
139
|
+
|
|
140
|
+
if err := authenticator.Validate(ctx); err != nil {
|
|
141
|
+
return fmt.Errorf("ADC validation failed: %w", err)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fmt.Println("✅ ADC authentication successful!")
|
|
145
|
+
fmt.Printf("Provider: %s\n", provider)
|
|
146
|
+
|
|
147
|
+
return nil
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
func runAPIKeyLogin(provider string, store auth.CredentialStore) error {
|
|
151
|
+
var envVar string
|
|
152
|
+
switch provider {
|
|
153
|
+
case "openai":
|
|
154
|
+
envVar = "OPENAI_API_KEY"
|
|
155
|
+
case "gemini":
|
|
156
|
+
envVar = "GEMINI_API_KEY or GOOGLE_API_KEY"
|
|
157
|
+
case "claude", "anthropic":
|
|
158
|
+
envVar = "ANTHROPIC_API_KEY"
|
|
159
|
+
default:
|
|
160
|
+
return fmt.Errorf("unknown provider: %s", provider)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fmt.Printf("🔑 API Key authentication for %s\n", provider)
|
|
164
|
+
fmt.Printf("Set environment variable: %s\n\n", envVar)
|
|
165
|
+
|
|
166
|
+
apiKey := os.Getenv(envVar)
|
|
167
|
+
if apiKey == "" && provider == "gemini" {
|
|
168
|
+
apiKey = os.Getenv("GOOGLE_API_KEY")
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if apiKey == "" {
|
|
172
|
+
fmt.Println("❌ API key not found in environment")
|
|
173
|
+
fmt.Printf("\nTo set up API key:\n")
|
|
174
|
+
fmt.Printf("export %s=your-api-key\n", envVar)
|
|
175
|
+
return fmt.Errorf("API key not configured")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Store credentials
|
|
179
|
+
creds := &auth.Credentials{
|
|
180
|
+
Type: auth.AuthTypeAPIKey,
|
|
181
|
+
Provider: provider,
|
|
182
|
+
Token: apiKey,
|
|
183
|
+
Metadata: make(map[string]string),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if err := store.Save(provider, creds); err != nil {
|
|
187
|
+
fmt.Printf("⚠️ Failed to store credentials: %v\n", err)
|
|
188
|
+
fmt.Println("Credentials are still available from environment variable")
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fmt.Println("✅ API key authentication successful!")
|
|
192
|
+
fmt.Printf("Provider: %s\n", provider)
|
|
193
|
+
|
|
194
|
+
return nil
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func runLogout(cmd *cobra.Command, args []string) error {
|
|
198
|
+
provider, _ := cmd.Flags().GetString("provider")
|
|
199
|
+
|
|
200
|
+
store := auth.GetDefaultStore()
|
|
201
|
+
|
|
202
|
+
if provider == "" {
|
|
203
|
+
// Logout from all providers
|
|
204
|
+
providers, err := store.List()
|
|
205
|
+
if err != nil {
|
|
206
|
+
return fmt.Errorf("failed to list providers: %w", err)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if len(providers) == 0 {
|
|
210
|
+
fmt.Println("No stored credentials found")
|
|
211
|
+
return nil
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fmt.Printf("Logging out from %d provider(s)...\n", len(providers))
|
|
215
|
+
for _, p := range providers {
|
|
216
|
+
if err := store.Delete(p); err != nil {
|
|
217
|
+
fmt.Printf("⚠️ Failed to remove %s: %v\n", p, err)
|
|
218
|
+
} else {
|
|
219
|
+
fmt.Printf("✅ Removed credentials for %s\n", p)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
// Logout from specific provider
|
|
224
|
+
if err := store.Delete(provider); err != nil {
|
|
225
|
+
return fmt.Errorf("failed to remove credentials: %w", err)
|
|
226
|
+
}
|
|
227
|
+
fmt.Printf("✅ Logged out from %s\n", provider)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return nil
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
func runStatus(cmd *cobra.Command, args []string) error {
|
|
234
|
+
fmt.Println("🔐 Authentication Status\n")
|
|
235
|
+
|
|
236
|
+
cfg, err := config.LoadConfig()
|
|
237
|
+
if err != nil {
|
|
238
|
+
return fmt.Errorf("failed to load config: %w", err)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
factory := auth.NewFactory(auth.GetDefaultStore())
|
|
242
|
+
ctx := context.Background()
|
|
243
|
+
|
|
244
|
+
// Check configured provider
|
|
245
|
+
fmt.Printf("Configured Provider: %s\n", cfg.Auth.Provider)
|
|
246
|
+
fmt.Printf("Auth Type: %s\n\n", cfg.Auth.Type)
|
|
247
|
+
|
|
248
|
+
// Check stored credentials
|
|
249
|
+
store := auth.GetDefaultStore()
|
|
250
|
+
providers, err := store.List()
|
|
251
|
+
if err != nil {
|
|
252
|
+
fmt.Printf("⚠️ Failed to list stored credentials: %v\n", err)
|
|
253
|
+
} else if len(providers) > 0 {
|
|
254
|
+
fmt.Println("Stored Credentials:")
|
|
255
|
+
for _, provider := range providers {
|
|
256
|
+
authenticator, err := factory.CreateFromProvider(provider)
|
|
257
|
+
if err != nil {
|
|
258
|
+
fmt.Printf(" • %s: ❌ Invalid\n", provider)
|
|
259
|
+
continue
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
info, err := getTokenInfo(ctx, authenticator)
|
|
263
|
+
if err != nil {
|
|
264
|
+
fmt.Printf(" • %s: ❌ Error: %v\n", provider, err)
|
|
265
|
+
continue
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
status := "✅ Valid"
|
|
269
|
+
if !info.Valid {
|
|
270
|
+
status = "❌ Invalid"
|
|
271
|
+
} else if !info.ExpiresAt.IsZero() && info.ExpiresAt.Before(time.Now()) {
|
|
272
|
+
status = "⚠️ Expired"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fmt.Printf(" • %s: %s", provider, status)
|
|
276
|
+
if !info.ExpiresAt.IsZero() {
|
|
277
|
+
fmt.Printf(" (expires in %s)", time.Until(info.ExpiresAt).Round(time.Minute))
|
|
278
|
+
}
|
|
279
|
+
fmt.Println()
|
|
280
|
+
}
|
|
281
|
+
fmt.Println()
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Check environment variables
|
|
285
|
+
fmt.Println("Environment Variables:")
|
|
286
|
+
checkEnvVar("OPENAI_API_KEY")
|
|
287
|
+
checkEnvVar("GEMINI_API_KEY")
|
|
288
|
+
checkEnvVar("GOOGLE_API_KEY")
|
|
289
|
+
checkEnvVar("ANTHROPIC_API_KEY")
|
|
290
|
+
fmt.Println()
|
|
291
|
+
|
|
292
|
+
// Check ADC
|
|
293
|
+
fmt.Print("Application Default Credentials (ADC): ")
|
|
294
|
+
if auth.IsADCAvailable(ctx) {
|
|
295
|
+
fmt.Println("✅ Available")
|
|
296
|
+
} else {
|
|
297
|
+
fmt.Println("❌ Not available")
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return nil
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
func checkEnvVar(name string) {
|
|
304
|
+
value := os.Getenv(name)
|
|
305
|
+
if value != "" {
|
|
306
|
+
masked := value[:minInt(8, len(value))] + "..."
|
|
307
|
+
fmt.Printf(" • %s: ✅ Set (%s)\n", name, masked)
|
|
308
|
+
} else {
|
|
309
|
+
fmt.Printf(" • %s: ❌ Not set\n", name)
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
func getTokenInfo(ctx context.Context, authenticator auth.Authenticator) (*auth.TokenInfo, error) {
|
|
314
|
+
// Try type assertion for method that returns TokenInfo
|
|
315
|
+
switch a := authenticator.(type) {
|
|
316
|
+
case *auth.APIKeyAuth:
|
|
317
|
+
return a.GetTokenInfo(ctx)
|
|
318
|
+
case *auth.OAuth2Auth:
|
|
319
|
+
return a.GetTokenInfo(ctx)
|
|
320
|
+
case *auth.ADCAuth:
|
|
321
|
+
return a.GetTokenInfo(ctx)
|
|
322
|
+
case *auth.VertexAIAuth:
|
|
323
|
+
return a.GetTokenInfo(ctx)
|
|
324
|
+
default:
|
|
325
|
+
// Fallback: just validate
|
|
326
|
+
if err := authenticator.Validate(ctx); err != nil {
|
|
327
|
+
return &auth.TokenInfo{Valid: false}, nil
|
|
328
|
+
}
|
|
329
|
+
return &auth.TokenInfo{Valid: true, Provider: authenticator.GetProvider()}, nil
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
func minInt(a, b int) int {
|
|
334
|
+
if a < b {
|
|
335
|
+
return a
|
|
336
|
+
}
|
|
337
|
+
return b
|
|
338
|
+
}
|
package/cmd/compose.go
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bufio"
|
|
5
|
+
"context"
|
|
6
|
+
"fmt"
|
|
7
|
+
"os"
|
|
8
|
+
"path/filepath"
|
|
9
|
+
"strings"
|
|
10
|
+
"time"
|
|
11
|
+
|
|
12
|
+
"github.com/ddhanush1/dcode/internal/agent"
|
|
13
|
+
"github.com/ddhanush1/dcode/internal/diffutil"
|
|
14
|
+
"github.com/ddhanush1/dcode/internal/fsutil"
|
|
15
|
+
"github.com/spf13/cobra"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
var (
|
|
19
|
+
composeFiles []string
|
|
20
|
+
composeAuto bool
|
|
21
|
+
composeModel string
|
|
22
|
+
composeProvider string
|
|
23
|
+
composeWeb bool
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
var composeCmd = &cobra.Command{
|
|
27
|
+
Use: "compose [prompt]",
|
|
28
|
+
Short: "Cursor-like composer for multi-file edits",
|
|
29
|
+
Long: `Composer Mode - Multi-file AI-powered code generation and editing
|
|
30
|
+
|
|
31
|
+
Like Cursor Composer, this mode allows you to:
|
|
32
|
+
• Generate multiple files at once
|
|
33
|
+
• Edit across multiple files simultaneously
|
|
34
|
+
• Create entire features or components
|
|
35
|
+
• Refactor with context awareness
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
dcode compose "Create a REST API with user CRUD operations"
|
|
39
|
+
dcode compose "Add authentication to the project"
|
|
40
|
+
dcode compose --files "*.go" "Add logging to all functions"
|
|
41
|
+
dcode compose --auto "Refactor database layer to use repository pattern"
|
|
42
|
+
|
|
43
|
+
The composer will:
|
|
44
|
+
1. Analyze your request and codebase
|
|
45
|
+
2. Determine which files need changes
|
|
46
|
+
3. Generate or edit multiple files
|
|
47
|
+
4. Show you a preview of all changes
|
|
48
|
+
5. Apply changes after confirmation (or --auto)
|
|
49
|
+
`,
|
|
50
|
+
RunE: runCompose,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func runCompose(cmd *cobra.Command, args []string) error {
|
|
54
|
+
if len(args) == 0 {
|
|
55
|
+
return fmt.Errorf("prompt required")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
prompt := strings.Join(args, " ")
|
|
59
|
+
|
|
60
|
+
if composeProvider != "" {
|
|
61
|
+
_ = os.Setenv("AI_PROVIDER", composeProvider)
|
|
62
|
+
}
|
|
63
|
+
if composeModel != "" {
|
|
64
|
+
_ = os.Setenv("AI_MODEL", composeModel)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ag, err := agent.New()
|
|
68
|
+
if err != nil {
|
|
69
|
+
return err
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
wd, _ := os.Getwd()
|
|
73
|
+
|
|
74
|
+
fmt.Println("\033[1;35m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m")
|
|
75
|
+
fmt.Println("\033[1;36m🎼 DCode Composer Mode\033[0m")
|
|
76
|
+
fmt.Printf("\033[90mWorking directory: %s\033[0m\n", wd)
|
|
77
|
+
fmt.Println("\033[1;35m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m")
|
|
78
|
+
fmt.Println()
|
|
79
|
+
|
|
80
|
+
// Gather context files
|
|
81
|
+
contextFiles, err := gatherContextFiles(wd, composeFiles)
|
|
82
|
+
if err != nil {
|
|
83
|
+
return fmt.Errorf("gathering context: %w", err)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if len(contextFiles) > 0 {
|
|
87
|
+
fmt.Printf("\033[90m📁 Context files (%d):\033[0m\n", len(contextFiles))
|
|
88
|
+
for _, f := range contextFiles {
|
|
89
|
+
fmt.Printf(" • %s\n", f.Path)
|
|
90
|
+
}
|
|
91
|
+
fmt.Println()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Build prompt with context
|
|
95
|
+
fullPrompt := buildComposePrompt(prompt, contextFiles)
|
|
96
|
+
|
|
97
|
+
fmt.Println("\033[1;33m🤔 Analyzing request...\033[0m")
|
|
98
|
+
fmt.Println()
|
|
99
|
+
|
|
100
|
+
// Use the agent's edit capability for multi-file operations
|
|
101
|
+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
102
|
+
defer cancel()
|
|
103
|
+
|
|
104
|
+
resp, err := ag.Edit(ctx, agent.EditRequest{
|
|
105
|
+
Prompt: fullPrompt,
|
|
106
|
+
Files: contextFiles,
|
|
107
|
+
WebSearch: agent.WebSearchRequest{Enabled: composeWeb},
|
|
108
|
+
})
|
|
109
|
+
if err != nil {
|
|
110
|
+
return fmt.Errorf("composer failed: %w", err)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if len(resp.Edits) == 0 {
|
|
114
|
+
fmt.Println("\033[33m⚠ No changes generated\033[0m")
|
|
115
|
+
return nil
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Display results
|
|
119
|
+
fmt.Println("\033[1;32m✨ Generated changes:\033[0m")
|
|
120
|
+
fmt.Println()
|
|
121
|
+
|
|
122
|
+
for i, edit := range resp.Edits {
|
|
123
|
+
fmt.Printf("\033[1;36m%d. %s\033[0m\n", i+1, edit.Path)
|
|
124
|
+
|
|
125
|
+
// Check if file exists
|
|
126
|
+
existingContent := ""
|
|
127
|
+
fullPath := filepath.Join(wd, edit.Path)
|
|
128
|
+
if _, err := os.Stat(fullPath); err == nil {
|
|
129
|
+
existingContent, _ = fsutil.ReadFile(fullPath)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if existingContent == "" {
|
|
133
|
+
fmt.Println("\033[90m (new file)\033[0m")
|
|
134
|
+
// Show preview of new file (first 10 lines)
|
|
135
|
+
lines := strings.Split(edit.NewContent, "\n")
|
|
136
|
+
preview := strings.Join(lines[:min(10, len(lines))], "\n")
|
|
137
|
+
fmt.Println("\033[90m" + preview + "\033[0m")
|
|
138
|
+
if len(lines) > 10 {
|
|
139
|
+
fmt.Printf("\033[90m ... (%d more lines)\033[0m\n", len(lines)-10)
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// Show diff
|
|
143
|
+
diff := diffutil.Unified("a/"+edit.Path, "b/"+edit.Path, existingContent, edit.NewContent)
|
|
144
|
+
fmt.Println(diff)
|
|
145
|
+
}
|
|
146
|
+
fmt.Println()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if resp.Explanation != "" {
|
|
150
|
+
fmt.Println("\033[1;34m💡 Explanation:\033[0m")
|
|
151
|
+
fmt.Println(resp.Explanation)
|
|
152
|
+
fmt.Println()
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Auto-apply or ask for confirmation
|
|
156
|
+
apply := composeAuto
|
|
157
|
+
if !apply {
|
|
158
|
+
fmt.Print("\033[1;33m📝 Apply these changes? [y/N] \033[0m")
|
|
159
|
+
reader := bufio.NewReader(os.Stdin)
|
|
160
|
+
line, _ := reader.ReadString('\n')
|
|
161
|
+
apply = strings.TrimSpace(strings.ToLower(line)) == "y" || strings.TrimSpace(strings.ToLower(line)) == "yes"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if !apply {
|
|
165
|
+
fmt.Println("\033[90m✗ Changes not applied\033[0m")
|
|
166
|
+
return nil
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Apply changes
|
|
170
|
+
fmt.Println("\033[1;32m✓ Applying changes...\033[0m")
|
|
171
|
+
for _, edit := range resp.Edits {
|
|
172
|
+
fullPath := filepath.Join(wd, edit.Path)
|
|
173
|
+
|
|
174
|
+
// Ensure directory exists
|
|
175
|
+
dir := filepath.Dir(fullPath)
|
|
176
|
+
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
177
|
+
return fmt.Errorf("creating directory %s: %w", dir, err)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Write file
|
|
181
|
+
perm := fsutil.FilePermOrDefault(fullPath, 0o644)
|
|
182
|
+
if err := fsutil.WriteFileAtomic(fullPath, []byte(edit.NewContent), perm); err != nil {
|
|
183
|
+
return fmt.Errorf("writing %s: %w", fullPath, err)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fmt.Printf(" \033[32m✓\033[0m %s\n", edit.Path)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fmt.Println()
|
|
190
|
+
fmt.Println("\033[1;32m🎉 All changes applied successfully!\033[0m")
|
|
191
|
+
|
|
192
|
+
return nil
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
func gatherContextFiles(wd string, patterns []string) ([]agent.FileInput, error) {
|
|
196
|
+
if len(patterns) == 0 {
|
|
197
|
+
// Auto-detect relevant files based on common patterns
|
|
198
|
+
patterns = []string{"*.go", "*.ts", "*.tsx", "*.js", "*.jsx", "*.py", "*.java", "*.rs"}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
var files []agent.FileInput
|
|
202
|
+
seen := make(map[string]bool)
|
|
203
|
+
|
|
204
|
+
for _, pattern := range patterns {
|
|
205
|
+
matches, err := filepath.Glob(filepath.Join(wd, pattern))
|
|
206
|
+
if err != nil {
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for _, match := range matches {
|
|
211
|
+
relPath, _ := filepath.Rel(wd, match)
|
|
212
|
+
if seen[relPath] {
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
seen[relPath] = true
|
|
216
|
+
|
|
217
|
+
// Skip large files and binaries
|
|
218
|
+
info, err := os.Stat(match)
|
|
219
|
+
if err != nil || info.IsDir() || info.Size() > 100*1024 {
|
|
220
|
+
continue
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
content, err := fsutil.ReadFile(match)
|
|
224
|
+
if err != nil {
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
files = append(files, agent.FileInput{
|
|
229
|
+
Path: filepath.ToSlash(relPath),
|
|
230
|
+
Content: content,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Limit context size
|
|
234
|
+
if len(files) >= 20 {
|
|
235
|
+
break
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return files, nil
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
func buildComposePrompt(userPrompt string, contextFiles []agent.FileInput) string {
|
|
244
|
+
var sb strings.Builder
|
|
245
|
+
|
|
246
|
+
sb.WriteString("You are a Cursor-like AI composer assistant. ")
|
|
247
|
+
sb.WriteString("Generate or edit multiple files as needed to fulfill the user's request.\n\n")
|
|
248
|
+
|
|
249
|
+
sb.WriteString("User Request:\n")
|
|
250
|
+
sb.WriteString(userPrompt)
|
|
251
|
+
sb.WriteString("\n\n")
|
|
252
|
+
|
|
253
|
+
if len(contextFiles) > 0 {
|
|
254
|
+
sb.WriteString("Existing Codebase Context:\n")
|
|
255
|
+
sb.WriteString("Analyze these files to understand the project structure, patterns, and style.\n")
|
|
256
|
+
sb.WriteString("Maintain consistency with existing code.\n\n")
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
sb.WriteString("Instructions:\n")
|
|
260
|
+
sb.WriteString("1. Create new files or edit existing ones as needed\n")
|
|
261
|
+
sb.WriteString("2. Follow best practices and existing code patterns\n")
|
|
262
|
+
sb.WriteString("3. Add comments where helpful\n")
|
|
263
|
+
sb.WriteString("4. Ensure code is production-ready\n")
|
|
264
|
+
sb.WriteString("5. Generate complete, runnable code\n")
|
|
265
|
+
|
|
266
|
+
return sb.String()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
func min(a, b int) int {
|
|
270
|
+
if a < b {
|
|
271
|
+
return a
|
|
272
|
+
}
|
|
273
|
+
return b
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
func init() {
|
|
277
|
+
composeCmd.Flags().StringSliceVarP(&composeFiles, "files", "f", nil, "File patterns for context (e.g., *.go, src/*.ts)")
|
|
278
|
+
composeCmd.Flags().BoolVarP(&composeAuto, "auto", "a", false, "Auto-apply changes without confirmation")
|
|
279
|
+
composeCmd.Flags().StringVar(&composeModel, "model", "", "AI model to use")
|
|
280
|
+
composeCmd.Flags().StringVar(&composeProvider, "provider", "", "AI provider (openai, claude, gemini)")
|
|
281
|
+
composeCmd.Flags().BoolVar(&composeWeb, "web", false, "Enable web search for additional context")
|
|
282
|
+
|
|
283
|
+
rootCmd.AddCommand(composeCmd)
|
|
284
|
+
}
|