diff --git a/.gitignore b/.gitignore index f4e2c6d..c6c8c34 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ dist/ *.tsbuildinfo +bin/gofumpt diff --git a/cli/cmd/engine-cli/display.go b/cli/cmd/engine-cli/display.go new file mode 100644 index 0000000..12b81c2 --- /dev/null +++ b/cli/cmd/engine-cli/display.go @@ -0,0 +1,297 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/github/copilot-engine-sdk/cli/internal/events" + "github.com/github/copilot-engine-sdk/cli/internal/server" +) + +var ( + dimStyle = lipgloss.NewStyle().Faint(true) + boldStyle = lipgloss.NewStyle().Bold(true) + greenStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")) + redStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("1")) + yellowStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("3")) + cyanStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")) + magStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("5")) + mutedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) + + separator = dimStyle.Render(" ─────────────────────────────────────────") +) + +func resolveKind(envelopeKind string, content json.RawMessage) string { + if envelopeKind != "" && envelopeKind != "log" { + return envelopeKind + } + var m struct { + Kind string `json:"kind"` + } + if json.Unmarshal(content, &m) == nil && m.Kind != "" { + return m.Kind + } + return "unknown" +} + +// isDisplayEvent returns true for events we render. +func isDisplayEvent(kind string) bool { + switch kind { + case "message", "tool_execution", "report_progress", "pr_summary", + "model_call_failure", "comment_reply": + return true + default: + return false + } +} + +// printEvent renders a single event. Returns true if something was printed. +func printEvent(event server.ProgressEvent) bool { + kind := resolveKind(event.Kind, event.Content) + + switch kind { + case "message": + return printMessage(event.Content) + case "tool_execution": + return printToolExecution(event.Content) + case "report_progress": + return printReportProgress(event.Content) + case "pr_summary": + return printPRSummary(event.Content) + case "model_call_failure": + return printModelCallFailure(event.Content) + case "comment_reply": + return printCommentReply(event.Content) + default: + return false + } +} + +// ───────────────────────────────────────────────────────────── +// Assistant messages +// ───────────────────────────────────────────────────────────── + +func printMessage(raw json.RawMessage) bool { + var ev events.Message + if json.Unmarshal(raw, &ev) != nil { + return false + } + content := strings.TrimSpace(ev.Message.Content) + if content == "" { + return false + } + + switch ev.Message.Role { + case "assistant": + // Skip raw XML wrapper messages (pr_title/pr_description) + if strings.HasPrefix(content, "") { + return false + } + fmt.Println(separator) + fmt.Printf(" %s %s\n", cyanStyle.Render("●"), boldStyle.Render("Assistant")) + fmt.Println() + for _, line := range wrapText(content, 74) { + fmt.Printf(" %s\n", line) + } + return true + + case "tool": + name := ev.ToolName + if name == "" { + name = "tool" + } + // Skip report_progress tool results — shown via ↑ Progress block + if strings.Contains(name, "report_progress") { + return false + } + fmt.Println(separator) + isErr := containsError(content) + if isErr { + fmt.Printf(" %s %s %s\n", redStyle.Render("●"), dimStyle.Render("Tool:"), yellowStyle.Render(name)) + } else { + fmt.Printf(" %s %s %s\n", greenStyle.Render("●"), dimStyle.Render("Tool:"), yellowStyle.Render(name)) + } + // Show the tool output, indented and muted + fmt.Println() + lines := wrapText(content, 70) + maxLines := 8 + for i, line := range lines { + if i >= maxLines { + fmt.Printf(" %s\n", mutedStyle.Render(fmt.Sprintf("... (%d more lines)", len(lines)-i))) + break + } + fmt.Printf(" %s\n", mutedStyle.Render(line)) + } + return true + + default: + return false + } +} + +// ───────────────────────────────────────────────────────────── +// Tool execution results +// ───────────────────────────────────────────────────────────── + +func printToolExecution(raw json.RawMessage) bool { + var ev events.ToolExecution + if json.Unmarshal(raw, &ev) != nil { + return false + } + name := ev.ToolName + if name == "" { + name = truncate(ev.ToolCallID, 20) + } + // Skip report_progress — already shown via ↑ Progress block + if strings.Contains(name, "report_progress") { + return false + } + // tool_execution is redundant with the tool message above — skip it + // The tool message (role=tool) already shows name + ✓/✗ + output + return false +} + +// ───────────────────────────────────────────────────────────── +// Progress updates +// ───────────────────────────────────────────────────────────── + +func printReportProgress(raw json.RawMessage) bool { + var ev events.ReportProgress + if json.Unmarshal(raw, &ev) != nil { + return false + } + fmt.Println(separator) + title := ev.PRTitle + if title == "" { + title = "Progress" + } + fmt.Printf(" %s %s\n", greenStyle.Render("●"), boldStyle.Render(title)) + if ev.PRDescription != "" { + fmt.Println() + for _, line := range strings.Split(ev.PRDescription, "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + continue + } + switch { + case strings.HasPrefix(trimmed, "- [x]"): + fmt.Printf(" %s\n", greenStyle.Render(trimmed)) + case strings.HasPrefix(trimmed, "- [ ]"): + fmt.Printf(" %s\n", yellowStyle.Render(trimmed)) + default: + fmt.Printf(" %s\n", dimStyle.Render(trimmed)) + } + } + } + return true +} + +// ───────────────────────────────────────────────────────────── +// PR Summary +// ───────────────────────────────────────────────────────────── + +func printPRSummary(raw json.RawMessage) bool { + var ev events.PRSummary + if json.Unmarshal(raw, &ev) != nil { + return false + } + fmt.Println(separator) + fmt.Printf(" %s %s %s\n", magStyle.Render("●"), dimStyle.Render("PR"), boldStyle.Render(ev.PRTitle)) + if ev.PRDescription != "" { + fmt.Println() + lines := strings.Split(ev.PRDescription, "\n") + for i, line := range lines { + if i >= 15 { + fmt.Printf(" %s\n", mutedStyle.Render(fmt.Sprintf("... (%d more lines)", len(lines)-i))) + break + } + fmt.Printf(" %s\n", line) + } + } + return true +} + +// ───────────────────────────────────────────────────────────── +// Errors and misc +// ───────────────────────────────────────────────────────────── + +func printModelCallFailure(raw json.RawMessage) bool { + var ev events.ModelCallFailure + if json.Unmarshal(raw, &ev) != nil { + return false + } + if ev.ModelCall.Error != "" { + fmt.Println(separator) + fmt.Printf(" %s %s\n", redStyle.Render("●"), redStyle.Render(truncate(ev.ModelCall.Error, 72))) + return true + } + return false +} + +func printCommentReply(raw json.RawMessage) bool { + var ev events.CommentReply + if json.Unmarshal(raw, &ev) != nil { + return false + } + fmt.Println(separator) + fmt.Printf(" %s %s #%d\n", cyanStyle.Render("●"), dimStyle.Render("Reply to"), ev.CommentID) + if ev.Message != "" { + fmt.Println() + for _, line := range wrapText(ev.Message, 74) { + fmt.Printf(" %s\n", line) + } + } + return true +} + +// ───────────────────────────────────────────────────────────── +// Helpers +// ───────────────────────────────────────────────────────────── + +func containsError(s string) bool { + lower := strings.ToLower(s) + return strings.Contains(lower, "error") || + strings.Contains(lower, "denied") || + strings.Contains(lower, "failed") || + strings.Contains(lower, "not found") +} + +func wrapText(text string, width int) []string { + var lines []string + for _, paragraph := range strings.Split(text, "\n") { + if len(paragraph) <= width { + lines = append(lines, paragraph) + continue + } + remaining := paragraph + for len(remaining) > width { + idx := width + for idx > 0 && remaining[idx] != ' ' { + idx-- + } + if idx == 0 { + idx = width + } + lines = append(lines, remaining[:idx]) + remaining = remaining[idx:] + if len(remaining) > 0 && remaining[0] == ' ' { + remaining = remaining[1:] + } + } + if len(remaining) > 0 { + lines = append(lines, remaining) + } + } + return lines +} + +func truncate(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen-3] + "..." +} diff --git a/cli/cmd/engine-cli/main.go b/cli/cmd/engine-cli/main.go index 9b9f7ca..2f64e2c 100644 --- a/cli/cmd/engine-cli/main.go +++ b/cli/cmd/engine-cli/main.go @@ -7,16 +7,12 @@ import ( "context" "encoding/json" "fmt" - "net/url" "os" - "os/exec" "os/signal" - "path/filepath" "strings" "syscall" "time" - "github.com/github/copilot-engine-sdk/cli/internal/events" "github.com/github/copilot-engine-sdk/cli/internal/runner" "github.com/github/copilot-engine-sdk/cli/internal/server" "github.com/github/copilot-engine-sdk/cli/internal/store" @@ -31,6 +27,7 @@ var ( workingDir string timeout time.Duration verbose bool + engineLogs bool action string commitLogin string commitEmail string @@ -85,6 +82,7 @@ func init() { runCmd.Flags().StringVarP(&workingDir, "working-dir", "w", "", "Working directory for the engine command (not the repo)") runCmd.Flags().DurationVarP(&timeout, "timeout", "t", 5*time.Minute, "Timeout for engine execution") runCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") + runCmd.Flags().BoolVar(&engineLogs, "engine-logs", false, "Show raw engine stdout/stderr instead of formatted server events") runCmd.Flags().StringVar(&action, "action", "fix", "Agent action type: fix, fix-pr-comment, or task") runCmd.Flags().StringVar(&commitLogin, "commit-login", "engine-cli-user", "Git author name for commits") runCmd.Flags().StringVar(&commitEmail, "commit-email", "engine-cli@users.noreply.github.com", "Git author email for commits") @@ -96,22 +94,6 @@ func init() { func runEngine(cmd *cobra.Command, args []string) error { command := args[0] - // Clone the repository to a temporary directory - fmt.Println("🚀 Engine Test Harness") - fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - fmt.Printf("📦 Cloning repository: %s\n", repoURL) - - repoDir, branchName, err := cloneRepo(repoURL) - if err != nil { - return fmt.Errorf("failed to clone repository: %w", err) - } - defer func() { - fmt.Printf("🧹 Cleaning up temp directory: %s\n", repoDir) - _ = os.RemoveAll(repoDir) - }() - - fmt.Printf("📁 Cloned to: %s\n", repoDir) - // Generate a job ID jobID := fmt.Sprintf("test-job-%d", time.Now().UnixNano()) @@ -121,6 +103,37 @@ func runEngine(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid repo URL: %w", err) } + // All engine tokens are derived from a single GITHUB_TOKEN in the environment. + githubToken := os.Getenv("GITHUB_TOKEN") + if githubToken == "" { + return fmt.Errorf("GITHUB_TOKEN must be set in the environment") + } + + // Determine owner/repo and API base URL + parts := strings.SplitN(repoNWO, "/", 2) + owner, repo := parts[0], parts[1] + apiBaseURL := serverURL + "/api/v3" + if strings.Contains(serverURL, "github.com") { + apiBaseURL = "https://api.github.com" + } + + // Fetch the default branch + defaultBranch, err := getDefaultBranch(apiBaseURL, githubToken, owner, repo) + if err != nil { + return fmt.Errorf("failed to get default branch: %w", err) + } + + branchName := fmt.Sprintf("engine-cli-test-%d", time.Now().UnixNano()) + + // Create branch with empty commit and draft PR + setup, err := setupBranchAndPR(apiBaseURL, githubToken, owner, repo, branchName, defaultBranch, + fmt.Sprintf("[engine-cli] %s", truncate(problemStatement, 60)), + fmt.Sprintf("**Problem:** %s\n\n_Created by engine-cli test harness._", problemStatement), + ) + if err != nil { + return fmt.Errorf("failed to set up branch and PR: %w", err) + } + // Create mock server jobConfig := server.JobConfig{ JobID: jobID, @@ -134,12 +147,38 @@ func runEngine(cmd *cobra.Command, args []string) error { CommitEmail: commitEmail, } + prNumber := setup.PRNumber + + // Track suppressed events for the summary line + suppressedCount := 0 + callbacks := server.Callbacks{ OnJobFetched: func() { - fmt.Println("📋 Engine fetched job details") + fmt.Println(separator) + fmt.Printf(" %s %s\n", greenStyle.Render("●"), dimStyle.Render("Job details fetched")) }, OnProgressEvent: func(event server.ProgressEvent) { - printProgressEvent(event) + kind := resolveKind(event.Kind, event.Content) + + // Update PR on progress and summary events + if kind == "report_progress" || kind == "pr_summary" { + var ev struct { + PRTitle string `json:"pr_title"` + PRDescription string `json:"pr_description"` + } + if json.Unmarshal(event.Content, &ev) == nil && (ev.PRTitle != "" || ev.PRDescription != "") { + _ = updatePullRequest(apiBaseURL, githubToken, owner, repo, prNumber, ev.PRTitle, ev.PRDescription) + } + } + + if engineLogs { + return + } + + // Try to print the event; if nothing was printed, count it as suppressed + if !printEvent(event) { + suppressedCount++ + } }, } @@ -152,9 +191,6 @@ func runEngine(cmd *cobra.Command, args []string) error { fmt.Printf("⚠️ Failed to load history for assignment %s: %v\n", assignmentID, err) } else if len(previousEvents) > 0 { mockServer.SetPreviousEvents(previousEvents) - fmt.Printf("📂 Loaded %d history events from previous run (assignment: %s)\n", len(previousEvents), assignmentID) - } else { - fmt.Printf("📂 No previous history for assignment: %s\n", assignmentID) } } @@ -171,48 +207,42 @@ func runEngine(cmd *cobra.Command, args []string) error { apiURL := fmt.Sprintf("http://localhost:%d/agent", port) - // All engine tokens are derived from a single GITHUB_TOKEN in the environment. - githubToken := os.Getenv("GITHUB_TOKEN") - if githubToken == "" { - return fmt.Errorf("GITHUB_TOKEN must be set in the environment") - } - - fmt.Printf("📡 Mock server running on %s\n", apiURL) - fmt.Printf("🆔 Job ID: %s\n", jobID) - fmt.Printf("📝 Problem: %s\n", truncate(problemStatement, 50)) - fmt.Printf("⚙️ Command: %s\n", command) - fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") + // Print header fmt.Println() + fmt.Printf(" %s %s\n", dimStyle.Render("repo:"), boldStyle.Render(repoNWO)) + fmt.Printf(" %s %s\n", dimStyle.Render("branch:"), mutedStyle.Render(branchName)) + if setup.PRURL != "" { + fmt.Printf(" %s %s\n", dimStyle.Render("pr:"), cyanStyle.Render(setup.PRURL)) + } + fmt.Printf(" %s %s\n", dimStyle.Render("job:"), mutedStyle.Render(jobID)) + fmt.Printf(" %s %s\n", dimStyle.Render("cmd:"), mutedStyle.Render(command)) + fmt.Printf(" %s %s\n", dimStyle.Render("timeout:"), mutedStyle.Render(timeout.String())) // Set up context with timeout and signal handling ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - // Handle interrupt signals - interrupted := false sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) go func() { <-sigChan - interrupted = true - fmt.Println("\n⚠️ Received interrupt, stopping engine...") cancel() }() // Create runner callbacks runnerCallbacks := runner.Callbacks{ OnStdout: func(line string) { - fmt.Printf("│ %s\n", line) + if engineLogs { + fmt.Printf(" %s %s\n", dimStyle.Render("│"), line) + } }, OnStderr: func(line string) { - fmt.Printf("│ \033[31m%s\033[0m\n", line) // Red for stderr + if engineLogs { + fmt.Printf(" %s %s\n", redStyle.Render("│"), line) + } }, } - // Run the engine - fmt.Println("▶️ Starting engine...") - fmt.Println() - env := runner.Environment{ JobID: jobID, APIToken: githubToken, @@ -222,24 +252,22 @@ func runEngine(cmd *cobra.Command, args []string) error { GitToken: githubToken, } - opts := runner.Options{ - WorkingDir: workingDir, - } - - result := runner.Run(ctx, command, env, opts, runnerCallbacks) + result := runner.Run(ctx, command, env, runner.Options{WorkingDir: workingDir}, runnerCallbacks) + // Summary + allEvents := mockServer.Events() + displayed := len(allEvents) - suppressedCount fmt.Println() - fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - - // Print summary - events := mockServer.Events() - fmt.Printf("📊 Summary:\n") - fmt.Printf(" Events received: %d\n", len(events)) + if result.ExitCode == 0 { + fmt.Printf(" %s %s\n", greenStyle.Render("✓"), mutedStyle.Render(fmt.Sprintf("Done (%d events, %d displayed)", len(allEvents), displayed))) + } else { + fmt.Printf(" %s %s\n", redStyle.Render("✗"), mutedStyle.Render(fmt.Sprintf("Failed with exit code %d (%d events)", result.ExitCode, len(allEvents)))) + } - // Save current events as history for next run - if assignmentID != "" && len(events) > 0 { - records := make([]store.Record, len(events)) - for i, ev := range events { + // Save history + if assignmentID != "" && len(allEvents) > 0 { + records := make([]store.Record, len(allEvents)) + for i, ev := range allEvents { records[i] = store.Record{ ID: fmt.Sprintf("progress-%d", i+1), Namespace: ev.Namespace, @@ -249,420 +277,11 @@ func runEngine(cmd *cobra.Command, args []string) error { CreatedAt: ev.Timestamp.Unix(), } } - if err := store.Save(assignmentID, records); err != nil { - fmt.Printf(" ⚠️ Failed to save history: %v\n", err) - } else { - fmt.Printf(" 💾 Saved %d events to history (assignment: %s)\n", len(records), assignmentID) - } - } - - if result.Error != nil { - fmt.Printf(" Error: %v\n", result.Error) - } - - // Print event summary by kind - eventCounts := make(map[string]int) - for _, e := range events { - kind := resolveKind(e.Kind, e.Content) - eventCounts[kind]++ - } - - if len(eventCounts) > 0 { - fmt.Println(" Event types:") - for kind, count := range eventCounts { - fmt.Printf(" - %s: %d\n", kind, count) - } - } - - fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") - - // Don't report error for interrupt or successful exit - if interrupted || result.ExitCode == 0 { - return nil + _ = store.Save(assignmentID, records) } if result.ExitCode != 0 { return fmt.Errorf("engine exited with code %d", result.ExitCode) } - return nil } - -// ANSI color codes -const ( - colorReset = "\033[0m" - colorBold = "\033[1m" - colorDim = "\033[2m" - colorCyan = "\033[36m" - colorGreen = "\033[32m" - colorYellow = "\033[33m" - colorBlue = "\033[34m" - colorMagenta = "\033[35m" - colorRed = "\033[31m" - colorWhite = "\033[37m" - colorGray = "\033[90m" -) - -func eventIcon(kind string) string { - switch kind { - case "message": - return "💬" - case "model_call_success": - return "✨" - case "model_call_failure": - return "❌" - case "tool_execution": - return "🔧" - case "response": - return "📤" - case "history_truncated": - return "✂️" - case "report_progress": - return "📝" - case "comment_reply": - return "💬" - case "pr_summary": - return "📋" - default: - return "📨" - } -} - -func eventColor(kind string) string { - switch kind { - case "message": - return colorCyan - case "model_call_success": - return colorGreen - case "model_call_failure": - return colorRed - case "tool_execution": - return colorYellow - case "response": - return colorMagenta - case "history_truncated": - return colorBlue - case "report_progress": - return colorGreen - case "comment_reply": - return colorCyan - case "pr_summary": - return colorMagenta - default: - return colorWhite - } -} - -// resolveKind extracts the semantic event kind. The progress envelope uses -// kind="log" for all regular sessions-v2 events, so we look inside the -// content JSON for the real kind in that case. -func resolveKind(envelopeKind string, content json.RawMessage) string { - if envelopeKind != "" && envelopeKind != "log" { - return envelopeKind - } - return extractKind(content) -} - -func printProgressEvent(event server.ProgressEvent) { - kind := resolveKind(event.Kind, event.Content) - icon := eventIcon(kind) - color := eventColor(kind) - - // Print event header with box drawing - fmt.Printf("┌─ %s %s%s%s%s\n", icon, color, colorBold, kind, colorReset) - - // Print event details - printEventDetails(kind, event.Content) - - if verbose { - // In verbose mode, show pretty-printed JSON - var raw any - if json.Unmarshal(event.Content, &raw) == nil { - pretty, _ := json.MarshalIndent(raw, "│ ", " ") - fmt.Printf("│ %s%s%s\n", colorGray, string(pretty), colorReset) - } - } - - fmt.Println("└─") -} - -func printEventDetails(kind string, raw json.RawMessage) { - switch kind { - case "message": - var ev events.Message - if json.Unmarshal(raw, &ev) != nil { - return - } - role := ev.Message.Role - roleColor := colorCyan - switch role { - case "assistant": - roleColor = colorGreen - case "user": - roleColor = colorYellow - case "tool": - roleColor = colorMagenta - } - - if role == "tool" && ev.ToolName != "" { - fmt.Printf("│ %sRole:%s %s%s%s %sTool:%s %s%s%s\n", - colorDim, colorReset, roleColor, role, colorReset, - colorDim, colorReset, colorYellow, ev.ToolName, colorReset) - } else { - fmt.Printf("│ %sRole:%s %s%s%s\n", colorDim, colorReset, roleColor, role, colorReset) - } - - if ev.Message.Content != "" { - printWrapped(ev.Message.Content, colorWhite, 3) - } - - case "model_call_success": - var ev events.ModelCallSuccess - if json.Unmarshal(raw, &ev) != nil { - return - } - if ev.ModelCall.Model != "" { - fmt.Printf("│ %sModel:%s %s\n", colorDim, colorReset, ev.ModelCall.Model) - } - if ev.ResponseUsage.PromptTokens > 0 || ev.ResponseUsage.CompletionTokens > 0 { - fmt.Printf("│ %sTokens:%s %s%d%s prompt → %s%d%s completion\n", - colorDim, colorReset, - colorYellow, ev.ResponseUsage.PromptTokens, colorReset, - colorGreen, ev.ResponseUsage.CompletionTokens, colorReset) - } - if len(ev.ResponseChunk.Choices) > 0 { - if text := ev.ResponseChunk.Choices[0].Delta.Content; text != "" { - printWrapped(text, colorGreen, 3) - } - } - - case "model_call_failure": - var ev events.ModelCallFailure - if json.Unmarshal(raw, &ev) != nil { - return - } - if ev.ModelCall.Error != "" { - fmt.Printf("│ %sError:%s %s%s%s\n", colorDim, colorReset, colorRed, truncate(ev.ModelCall.Error, 80), colorReset) - } - - case "tool_execution": - var ev events.ToolExecution - if json.Unmarshal(raw, &ev) != nil { - return - } - name := ev.ToolName - if name == "" { - name = truncate(ev.ToolCallID, 23) - } - status := ev.ToolResult.ResultType - statusColor := colorYellow - switch status { - case "success": - statusColor = colorGreen - case "failure", "error": - statusColor = colorRed - } - fmt.Printf("│ %sTool:%s %s%s%s %sStatus:%s %s%s%s\n", - colorDim, colorReset, colorYellow, name, colorReset, - colorDim, colorReset, statusColor, status, colorReset) - - case "response": - var ev events.Response - if json.Unmarshal(raw, &ev) != nil { - return - } - if ev.Response.Content != "" { - printWrapped(ev.Response.Content, colorMagenta, 3) - } - - case "history_truncated": - var ev events.HistoryTruncated - if json.Unmarshal(raw, &ev) != nil { - return - } - fmt.Printf("│ %sMessages:%s %s%d%s → %s%d%s\n", - colorDim, colorReset, - colorRed, ev.TruncateResult.PreTruncationMessagesLength, colorReset, - colorGreen, ev.TruncateResult.PostTruncationMessagesLength, colorReset) - - case "report_progress": - var ev events.ReportProgress - if json.Unmarshal(raw, &ev) != nil { - return - } - if ev.PRTitle != "" { - fmt.Printf("│ %sTitle:%s %s%s%s\n", colorDim, colorReset, colorBold, ev.PRTitle, colorReset) - } - if ev.PRDescription != "" { - lines := strings.Split(ev.PRDescription, "\n") - fmt.Printf("│ %sDescription:%s\n", colorDim, colorReset) - for i, line := range lines { - if i >= 10 { - fmt.Printf("│ %s... (%d more lines)%s\n", colorDim, len(lines)-i, colorReset) - break - } - trimmed := strings.TrimSpace(line) - switch { - case strings.HasPrefix(trimmed, "- [x]"): - fmt.Printf("│ %s%s%s\n", colorGreen, line, colorReset) - case strings.HasPrefix(trimmed, "- [ ]"): - fmt.Printf("│ %s%s%s\n", colorYellow, line, colorReset) - default: - fmt.Printf("│ %s\n", line) - } - } - } - - case "comment_reply": - var ev events.CommentReply - if json.Unmarshal(raw, &ev) != nil { - return - } - if ev.CommentID != 0 { - fmt.Printf("│ %sComment ID:%s %d\n", colorDim, colorReset, ev.CommentID) - } - if ev.Message != "" { - printWrapped(ev.Message, colorCyan, 5) - } - - case "pr_summary": - var ev events.PRSummary - if json.Unmarshal(raw, &ev) != nil { - return - } - fmt.Printf("│ %s━━━ Final PR Summary ━━━%s\n", colorBold, colorReset) - if ev.PRTitle != "" { - fmt.Printf("│ %sTitle:%s %s%s%s\n", colorDim, colorReset, colorBold+colorMagenta, ev.PRTitle, colorReset) - } - if ev.PRDescription != "" { - lines := strings.Split(ev.PRDescription, "\n") - fmt.Printf("│ %sDescription:%s\n", colorDim, colorReset) - for i, line := range lines { - if i >= 15 { - fmt.Printf("│ %s... (%d more lines)%s\n", colorDim, len(lines)-i, colorReset) - break - } - fmt.Printf("│ %s\n", line) - } - } - } -} - -func printWrapped(text, color string, maxLines int) { - wrapped := wrapText(text, 70) - for i, line := range wrapped { - if i == 0 { - fmt.Printf("│ %s\"%s\"%s\n", color, line, colorReset) - } else { - fmt.Printf("│ %s %s%s\n", color, line, colorReset) - } - if i >= maxLines-1 { - fmt.Printf("│ %s...%s\n", colorDim, colorReset) - break - } - } -} - -func wrapText(text string, width int) []string { - if len(text) <= width { - return []string{text} - } - - var lines []string - for len(text) > width { - // Find last space before width - idx := width - for idx > 0 && text[idx] != ' ' { - idx-- - } - if idx == 0 { - idx = width // No space found, hard break - } - lines = append(lines, text[:idx]) - text = text[idx:] - if len(text) > 0 && text[0] == ' ' { - text = text[1:] - } - } - if len(text) > 0 { - lines = append(lines, text) - } - return lines -} - -func extractKind(raw json.RawMessage) string { - var m struct { - Kind string `json:"kind"` - } - if json.Unmarshal(raw, &m) == nil && m.Kind != "" { - return m.Kind - } - return "unknown" -} - -func truncate(s string, maxLen int) string { - if len(s) <= maxLen { - return s - } - return s[:maxLen-3] + "..." -} - -// parseRepoURL splits a full repo URL into server URL and owner/repo. -// e.g. "https://github.com/josebalius/dotfiles" → ("https://github.com", "josebalius/dotfiles") -func parseRepoURL(raw string) (string, string, error) { - u, err := url.Parse(strings.TrimSuffix(raw, ".git")) - if err != nil { - return "", "", err - } - path := strings.TrimPrefix(u.Path, "/") - parts := strings.SplitN(path, "/", 3) - if len(parts) < 2 || parts[0] == "" || parts[1] == "" { - return "", "", fmt.Errorf("expected owner/repo in URL path, got %q", path) - } - serverURL := fmt.Sprintf("%s://%s", u.Scheme, u.Host) - nwo := parts[0] + "/" + parts[1] - return serverURL, nwo, nil -} - -// cloneRepo clones a GitHub repository to a temporary directory, creates a working branch, and returns the path. -func cloneRepo(repoURL string) (string, string, error) { - // Create a temporary directory - tempDir, err := os.MkdirTemp("", "engine-cli-repo-*") - if err != nil { - return "", "", fmt.Errorf("failed to create temp directory: %w", err) - } - - // Clone the repository - cmd := exec.Command("git", "clone", "--depth", "1", repoURL, tempDir) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - // Clean up temp dir on failure - _ = os.RemoveAll(tempDir) - return "", "", fmt.Errorf("git clone failed: %w", err) - } - - // Create and checkout a working branch - branchName := fmt.Sprintf("engine-cli-test-%d", time.Now().UnixNano()) - cmd = exec.Command("git", "checkout", "-b", branchName) - cmd.Dir = tempDir - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Run(); err != nil { - _ = os.RemoveAll(tempDir) - return "", "", fmt.Errorf("failed to create branch: %w", err) - } - - fmt.Printf("🌿 Created branch: %s\n", branchName) - - // Get absolute path - absPath, err := filepath.Abs(tempDir) - if err != nil { - _ = os.RemoveAll(tempDir) - return "", "", fmt.Errorf("failed to get absolute path: %w", err) - } - - return absPath, branchName, nil -} diff --git a/cli/cmd/engine-cli/repo.go b/cli/cmd/engine-cli/repo.go new file mode 100644 index 0000000..b1db99d --- /dev/null +++ b/cli/cmd/engine-cli/repo.go @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +// parseRepoURL splits a full repo URL into server URL and owner/repo. +// e.g. "https://github.com/josebalius/dotfiles" → ("https://github.com", "josebalius/dotfiles") +func parseRepoURL(raw string) (string, string, error) { + u, err := url.Parse(strings.TrimSuffix(raw, ".git")) + if err != nil { + return "", "", err + } + path := strings.TrimPrefix(u.Path, "/") + parts := strings.SplitN(path, "/", 3) + if len(parts) < 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("expected owner/repo in URL path, got %q", path) + } + serverURL := fmt.Sprintf("%s://%s", u.Scheme, u.Host) + nwo := parts[0] + "/" + parts[1] + return serverURL, nwo, nil +} + +// SetupResult contains the results of the branch + PR setup. +type SetupResult struct { + BranchName string + PRURL string + PRNumber int +} + +// setupBranchAndPR creates a branch with an empty commit and opens a draft PR. +func setupBranchAndPR(apiBaseURL, token, owner, repo, branchName, defaultBranch, prTitle, prBody string) (*SetupResult, error) { + apiURL := strings.TrimSuffix(apiBaseURL, "/") + + // 1. Get the HEAD SHA of the default branch + headSHA, treeSHA, err := getCommitInfo(apiURL, token, owner, repo, defaultBranch) + if err != nil { + return nil, fmt.Errorf("failed to get HEAD commit: %w", err) + } + + // 2. Create an empty commit (same tree as HEAD) + commitSHA, err := createEmptyCommit(apiURL, token, owner, repo, headSHA, treeSHA, "Initial plan") + if err != nil { + return nil, fmt.Errorf("failed to create empty commit: %w", err) + } + + // 3. Create the branch pointing at the empty commit + if err := createBranch(apiURL, token, owner, repo, branchName, commitSHA); err != nil { + return nil, fmt.Errorf("failed to create branch: %w", err) + } + + // 4. Create a draft pull request + prURL, prNumber, err := createPullRequest(apiURL, token, owner, repo, branchName, defaultBranch, prTitle, prBody) + if err != nil { + return nil, fmt.Errorf("failed to create pull request: %w", err) + } + + return &SetupResult{BranchName: branchName, PRURL: prURL, PRNumber: prNumber}, nil +} + +func getCommitInfo(apiURL, token, owner, repo, ref string) (commitSHA, treeSHA string, err error) { + url := fmt.Sprintf("%s/repos/%s/%s/commits/%s", apiURL, owner, repo, ref) + body, err := githubAPI("GET", url, token, nil) + if err != nil { + return "", "", err + } + var resp struct { + SHA string `json:"sha"` + Commit struct { + Tree struct { + SHA string `json:"sha"` + } `json:"tree"` + } `json:"commit"` + } + if err := json.Unmarshal(body, &resp); err != nil { + return "", "", err + } + return resp.SHA, resp.Commit.Tree.SHA, nil +} + +func createEmptyCommit(apiURL, token, owner, repo, parentSHA, treeSHA, message string) (string, error) { + url := fmt.Sprintf("%s/repos/%s/%s/git/commits", apiURL, owner, repo) + payload := map[string]any{ + "message": message, + "tree": treeSHA, + "parents": []string{parentSHA}, + } + body, err := githubAPI("POST", url, token, payload) + if err != nil { + return "", err + } + var resp struct { + SHA string `json:"sha"` + } + if err := json.Unmarshal(body, &resp); err != nil { + return "", err + } + return resp.SHA, nil +} + +func createBranch(apiURL, token, owner, repo, branchName, sha string) error { + url := fmt.Sprintf("%s/repos/%s/%s/git/refs", apiURL, owner, repo) + payload := map[string]string{ + "ref": "refs/heads/" + branchName, + "sha": sha, + } + _, err := githubAPI("POST", url, token, payload) + return err +} + +func createPullRequest(apiURL, token, owner, repo, head, base, title, body string) (string, int, error) { + url := fmt.Sprintf("%s/repos/%s/%s/pulls", apiURL, owner, repo) + payload := map[string]any{ + "title": title, + "body": body, + "head": head, + "base": base, + "draft": false, + } + respBody, err := githubAPI("POST", url, token, payload) + if err != nil { + return "", 0, err + } + var resp struct { + HTMLURL string `json:"html_url"` + Number int `json:"number"` + } + if err := json.Unmarshal(respBody, &resp); err != nil { + return "", 0, err + } + return resp.HTMLURL, resp.Number, nil +} + +func updatePullRequest(apiURL, token, owner, repo string, prNumber int, title, body string) error { + url := fmt.Sprintf("%s/repos/%s/%s/pulls/%d", apiURL, owner, repo, prNumber) + payload := map[string]any{} + if title != "" { + payload["title"] = title + } + if body != "" { + payload["body"] = body + } + _, err := githubAPI("PATCH", url, token, payload) + return err +} + +func githubAPI(method, url, token string, payload any) ([]byte, error) { + var reqBody io.Reader + if payload != nil { + data, err := json.Marshal(payload) + if err != nil { + return nil, err + } + reqBody = bytes.NewReader(data) + } + + req, err := http.NewRequest(method, url, reqBody) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "token "+token) + req.Header.Set("Accept", "application/vnd.github+json") + if payload != nil { + req.Header.Set("Content-Type", "application/json") + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode >= 300 { + return nil, fmt.Errorf("GitHub API %s %s returned %d: %s", method, url, resp.StatusCode, string(body)) + } + + return body, nil +} + +// getDefaultBranch fetches the repository's default branch name. +func getDefaultBranch(apiURL, token, owner, repo string) (string, error) { + url := fmt.Sprintf("%s/repos/%s/%s", apiURL, owner, repo) + body, err := githubAPI("GET", url, token, nil) + if err != nil { + return "", err + } + var resp struct { + DefaultBranch string `json:"default_branch"` + } + if err := json.Unmarshal(body, &resp); err != nil { + return "", err + } + return resp.DefaultBranch, nil +} diff --git a/cli/engine-cli b/cli/engine-cli index 0901ef5..ff86999 100755 Binary files a/cli/engine-cli and b/cli/engine-cli differ diff --git a/cli/go.mod b/cli/go.mod index df1913e..2f3c672 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -1,10 +1,29 @@ module github.com/github/copilot-engine-sdk/cli -go 1.23 +go 1.24.0 require github.com/spf13/cobra v1.8.0 require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.3.8 // indirect ) diff --git a/cli/go.sum b/cli/go.sum index d0e8c2c..53f8c32 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -1,10 +1,55 @@ +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= +github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/package.json b/package.json index a8fad33..c14db0f 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,9 @@ "dist" ], "scripts": { - "build": "npm run typecheck", + "build": "npm run typecheck && npm run bundle:mcp-server", "typecheck": "tsc", + "bundle:mcp-server": "esbuild src/mcp-server.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/mcp-server.bundled.js --banner:js=\"import { createRequire } from 'module';const require = createRequire(import.meta.url);\"", "prepare": "npm run build", "prepublishOnly": "npm run build" }, @@ -30,6 +31,7 @@ }, "devDependencies": { "@types/node": "^20.0.0", + "esbuild": "^0.25.10", "typescript": "^5.0.0" }, "engines": { diff --git a/pkg/sumdb/sum.golang.org/latest b/pkg/sumdb/sum.golang.org/latest new file mode 100644 index 0000000..b0a7ae3 --- /dev/null +++ b/pkg/sumdb/sum.golang.org/latest @@ -0,0 +1,5 @@ +go.sum database tree +50794107 +U00W9lZkrWx58fnkllP9K7cH7sZzLQ4R0VV8mlRicCc= + +— sum.golang.org Az3grnbO6ZNDhr/Z/PvNgPKTbsnLH7vL45pu4XUoFQc74qvp3lfAEgaaqLxBExmTIAE8Ke1Hk+TxpZh/UBfb+CenAQA=