Files
pack/core/plan/command.go
T
2025-03-04 00:39:55 -05:00

221 lines
6.0 KiB
Go

package plan
import (
"encoding/json"
"fmt"
"os"
"strings"
)
type Command interface {
CommandType() string
Spreadable
}
type ExecOptions struct {
CustomName string
}
// ExecCommand represents a shell command to be executed during the build
type ExecCommand struct {
Cmd string `json:"cmd" jsonschema:"description=The shell command to execute (e.g. 'go build' or 'npm install')"`
CustomName string `json:"customName,omitempty" jsonschema:"description=Optional custom name to display for this command in build output"`
}
// PathCommand represents adding a directory to the global PATH environment variable
type PathCommand struct {
Path string `json:"path" jsonschema:"description=Directory path to add to the global PATH environment variable. This path will be available to all subsequent commands in the build"`
}
// CopyCommand represents copying files or directories during the build
type CopyCommand struct {
Image string `json:"image,omitempty" jsonschema:"description=Optional source image to copy from. This can be any public image URL"`
Src string `json:"src" jsonschema:"description=Source path to copy from. Can be a file or directory"`
Dest string `json:"dest" jsonschema:"description=Destination path to copy to. Will be created if it doesn't exist"`
}
type FileOptions struct {
Mode os.FileMode
CustomName string
}
// FileCommand represents creating or modifying a file during the build
type FileCommand struct {
Path string `json:"path" jsonschema:"description=Directory path where the file should be created"`
Name string `json:"name" jsonschema:"description=Name of the file to create"`
Mode os.FileMode `json:"mode,omitempty" jsonschema:"description=Optional Unix file permissions mode (e.g. 0644 for regular file)"`
CustomName string `json:"customName,omitempty" jsonschema:"description=Optional custom name to display for this file operation"`
}
func (e ExecCommand) CommandType() string { return "exec" }
func (g PathCommand) CommandType() string { return "globalPath" }
func (c CopyCommand) CommandType() string { return "copy" }
func (f FileCommand) CommandType() string { return "file" }
func NewExecCommand(cmd string, options ...ExecOptions) Command {
exec := ExecCommand{Cmd: cmd}
if len(options) > 0 {
exec.CustomName = options[0].CustomName
}
return exec
}
func ShellCommandString(cmd string) string {
return "sh -c '" + cmd + "'"
}
func NewExecShellCommand(cmd string, options ...ExecOptions) Command {
if len(options) == 0 {
options = []ExecOptions{
{CustomName: cmd},
}
}
exec := NewExecCommand(ShellCommandString(cmd), options...)
return exec
}
func NewPathCommand(path string, customName ...string) Command {
pathCmd := PathCommand{Path: path}
return pathCmd
}
func NewCopyCommand(src string, dst ...string) Command {
dstPath := src
if len(dst) > 0 {
dstPath = dst[0]
}
copyCmd := CopyCommand{Src: src, Dest: dstPath}
return copyCmd
}
func NewFileCommand(path, name string, options ...FileOptions) Command {
fileCmd := FileCommand{Path: path, Name: name}
if len(options) > 0 {
fileCmd.CustomName = options[0].CustomName
fileCmd.Mode = options[0].Mode
}
return fileCmd
}
func UnmarshalCommand(data []byte) (Command, error) {
// First try to unmarshal as JSON object
if cmd, err := UnmarshalJsonCommand(data); err == nil {
return cmd, nil
}
// If that fails, parse the string into a command
return UnmarshalStringCommand(data)
}
func UnmarshalJsonCommand(data []byte) (Command, error) {
// Try to unmarshal as JSON object
var rawMap map[string]interface{}
if err := json.Unmarshal(data, &rawMap); err != nil {
return nil, err
}
// Determine command type based on fields present
if _, ok := rawMap["cmd"]; ok {
var cmd ExecCommand
if err := json.Unmarshal(data, &cmd); err != nil {
return nil, err
}
return cmd, nil
}
if _, ok := rawMap["path"]; ok {
if _, ok := rawMap["name"]; ok {
var file FileCommand
if err := json.Unmarshal(data, &file); err != nil {
return nil, err
}
return file, nil
}
var path PathCommand
if err := json.Unmarshal(data, &path); err != nil {
return nil, err
}
return path, nil
}
if _, ok := rawMap["src"]; ok {
var copy CopyCommand
if err := json.Unmarshal(data, &copy); err != nil {
return nil, err
}
return copy, nil
}
return nil, fmt.Errorf("unknown command type: %v", rawMap)
}
func UnmarshalStringCommand(data []byte) (Command, error) {
str := string(data)
// If no prefix, treat as exec command
if !strings.Contains(str, ":") {
cmdToRun := strings.Trim(str, "\"")
return NewExecShellCommand(cmdToRun, ExecOptions{CustomName: cmdToRun}), nil
}
parts := strings.SplitN(str, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid command format: %s", str)
}
prefix := parts[0]
payload := parts[1]
// Split prefix into command type and custom name
prefixParts := strings.SplitN(prefix, "#", 2)
cmdType := prefixParts[0]
customName := ""
if len(prefixParts) > 1 {
customName = prefixParts[1]
}
switch cmdType {
case "RUN":
return NewExecShellCommand(payload, ExecOptions{CustomName: customName}), nil
case "PATH":
return NewPathCommand(payload), nil
case "COPY":
copyParts := strings.Fields(payload)
if len(copyParts) != 2 {
return nil, fmt.Errorf("invalid COPY format: %s", payload)
}
return NewCopyCommand(copyParts[0], copyParts[1]), nil
case "FILE":
fileParts := strings.Fields(payload)
if len(fileParts) != 2 {
return nil, fmt.Errorf("invalid FILE format: %s", payload)
}
return NewFileCommand(fileParts[0], fileParts[1], FileOptions{CustomName: customName}), nil
}
// fallback to exec command type
cmdToRun := strings.Trim(str, "\"")
if customName == "" {
customName = cmdToRun
}
return NewExecShellCommand(cmdToRun, ExecOptions{CustomName: customName}), nil
}
func (e ExecCommand) IsSpread() bool {
return e.Cmd == ShellCommandString("...") || e.Cmd == "..."
}
func (p PathCommand) IsSpread() bool {
return false
}
func (c CopyCommand) IsSpread() bool {
return false
}
func (f FileCommand) IsSpread() bool {
return false
}