|
package main |
|
|
|
import ( |
|
"bytes" |
|
"context" |
|
"flag" |
|
"fmt" |
|
"io" |
|
"os" |
|
"os/exec" |
|
"strings" |
|
) |
|
|
|
// debug holds the state of the debug flag. |
|
var debug bool |
|
|
|
func main() { |
|
// Define and parse command-line flags. |
|
// If -debug is present, the global 'debug' variable will be set to true. |
|
flag.BoolVar(&debug, "debug", false, "Enable debug mode to show all underlying command outputs to stderr.") |
|
flag.Parse() |
|
|
|
// Get the non-flag arguments (package paths). |
|
pkgPaths := flag.Args() |
|
if len(pkgPaths) == 0 { |
|
fmt.Fprintf(os.Stderr, "Usage: %s [-debug] <package_path>[@version]...\n", os.Args[0]) |
|
os.Exit(1) |
|
} |
|
|
|
// Delegate the main logic to the run function to simplify error handling. |
|
if err := run(pkgPaths); err != nil { |
|
// If an error occurs during execution, print it to stderr and exit. |
|
fmt.Fprintln(os.Stderr, err) |
|
os.Exit(1) |
|
} |
|
} |
|
|
|
// run executes the main logic of the tool. |
|
func run(pkgPaths []string) error { |
|
ctx := context.Background() |
|
|
|
// Create a slice of package paths with any version suffixes stripped off. |
|
// This is necessary for 'go list'. |
|
strippedPkgPaths := make([]string, len(pkgPaths)) |
|
for i, p := range pkgPaths { |
|
strippedPkgPaths[i] = stripVersion(p) |
|
} |
|
|
|
// --- Step 1: Try running 'go list' in the current directory first. --- |
|
// We use the stripped paths here. |
|
output, err := goList(ctx, "", strippedPkgPaths) |
|
if err == nil { |
|
// If successful, write the result to stdout and exit cleanly. |
|
fmt.Print(string(output)) |
|
return nil |
|
} |
|
|
|
// If it fails, notify the user in debug mode. |
|
if debug { |
|
fmt.Fprintf(os.Stderr, "==> Initial 'go list' failed. Retrying in a temporary module...\n") |
|
// The detailed error is already printed inside the goList function. |
|
} |
|
|
|
// --- Step 2: Since it failed, create a temporary directory and retry. --- |
|
tmpDir, err := os.MkdirTemp("", "goname-") |
|
if err != nil { |
|
return fmt.Errorf("error creating temp directory: %w", err) |
|
} |
|
// Schedule the removal of the temporary directory when this function returns. |
|
defer os.RemoveAll(tmpDir) |
|
|
|
if debug { |
|
fmt.Fprintf(os.Stderr, "==> Created temporary directory: %s\n", tmpDir) |
|
} |
|
|
|
// Run 'go mod init m' inside the temporary directory. |
|
if err := goModInit(ctx, tmpDir); err != nil { |
|
return fmt.Errorf("failed to run 'go mod init': %w", err) |
|
} |
|
|
|
// Download the target packages using 'go get'. |
|
// We use the original paths here to respect any version specifiers. |
|
if err := goGet(ctx, tmpDir, pkgPaths); err != nil { |
|
return fmt.Errorf("failed to run 'go get': %w", err) |
|
} |
|
|
|
// Run 'go list' again to get the package names. |
|
// We use the stripped paths again. |
|
finalOutput, err := goList(ctx, tmpDir, strippedPkgPaths) |
|
if err != nil { |
|
return fmt.Errorf("failed to run 'go list' in temp directory: %w", err) |
|
} |
|
|
|
// Write the final result to stdout. |
|
fmt.Print(string(finalOutput)) |
|
return nil |
|
} |
|
|
|
// goList executes 'go list' in the specified directory and returns the package names (stdout). |
|
func goList(ctx context.Context, dir string, pkgPaths []string) ([]byte, error) { |
|
args := []string{"list", "-f", `{{.Name}}`} |
|
args = append(args, pkgPaths...) |
|
cmd := exec.CommandContext(ctx, "go", args...) |
|
cmd.Dir = dir // Set the execution directory (current directory if empty). |
|
|
|
var stdoutBuf, stderrBuf bytes.Buffer |
|
cmd.Stdout = &stdoutBuf |
|
|
|
// In debug mode, pipe the command's stderr to this process's stderr as well. |
|
if debug { |
|
// Use io.MultiWriter to write to both the buffer and os.Stderr. |
|
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) |
|
fmt.Fprintf(os.Stderr, "==> Executing in %s: go %s\n", dirForLog(dir), strings.Join(args, " ")) |
|
} else { |
|
cmd.Stderr = &stderrBuf |
|
} |
|
|
|
if err := cmd.Run(); err != nil { |
|
// If the command fails, return an error that includes the stderr output. |
|
return nil, fmt.Errorf("`go list` command failed: %w\n--- stderr ---\n%s", err, stderrBuf.String()) |
|
} |
|
|
|
return stdoutBuf.Bytes(), nil |
|
} |
|
|
|
// goModInit runs 'go mod init m' in the specified directory. |
|
func goModInit(ctx context.Context, dir string) error { |
|
args := []string{"mod", "init", "m"} |
|
cmd := exec.CommandContext(ctx, "go", args...) |
|
cmd.Dir = dir |
|
|
|
return executeSideEffectCommand(cmd) |
|
} |
|
|
|
// goGet runs 'go get' in the specified directory. |
|
func goGet(ctx context.Context, dir string, pkgPaths []string) error { |
|
args := []string{"get"} |
|
args = append(args, pkgPaths...) |
|
cmd := exec.CommandContext(ctx, "go", args...) |
|
cmd.Dir = dir |
|
|
|
return executeSideEffectCommand(cmd) |
|
} |
|
|
|
// executeSideEffectCommand is a helper for running commands like 'go mod init' or 'go get', |
|
// which are executed for their side effects (e.g., creating files) and don't need stdout capture. |
|
func executeSideEffectCommand(cmd *exec.Cmd) error { |
|
var stderrBuf bytes.Buffer |
|
if debug { |
|
// In debug mode, stream stdout and stderr directly to the process's stderr. |
|
cmd.Stdout = os.Stderr |
|
cmd.Stderr = io.MultiWriter(os.Stderr, &stderrBuf) // Also capture for error reporting |
|
fmt.Fprintf(os.Stderr, "==> Executing in %s: %s\n", dirForLog(cmd.Dir), cmd.String()) |
|
} else { |
|
// In normal mode, only capture stderr. |
|
cmd.Stderr = &stderrBuf |
|
} |
|
|
|
if err := cmd.Run(); err != nil { |
|
return fmt.Errorf("command `%s` failed: %w\n--- stderr ---\n%s", cmd.Args[0], err, stderrBuf.String()) |
|
} |
|
return nil |
|
} |
|
|
|
// stripVersion removes the version suffix (e.g., "@latest") from a package path. |
|
// If no '@' is found, it returns the original string. |
|
func stripVersion(pkgPath string) string { |
|
if i := strings.LastIndex(pkgPath, "@"); i != -1 { |
|
// Check if there is a '/' after the '@'. If so, it's likely a scoped package |
|
// from a private registry and not a version string. |
|
// e.g., [email protected]/pkg |
|
if !strings.Contains(pkgPath[i:], "/") { |
|
return pkgPath[:i] |
|
} |
|
} |
|
return pkgPath |
|
} |
|
|
|
// dirForLog returns "current directory" if the dir path is empty, for logging purposes. |
|
func dirForLog(dir string) string { |
|
if dir == "" { |
|
return "current directory" |
|
} |
|
return dir |
|
} |
https://aistudio.google.com/app/prompts?state=%7B%22ids%22:%5B%2214I_Y03l1QPcryUVPKFnnoZQ2r4zANxYr%22%5D,%22action%22:%22open%22,%22userId%22:%22108405443477417806091%22,%22resourceKeys%22:%7B%7D%7D&usp=sharing