-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcontract_opencli.go
More file actions
155 lines (140 loc) · 4.24 KB
/
Copy pathcontract_opencli.go
File metadata and controls
155 lines (140 loc) · 4.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package exoskeleton
import (
"encoding/json"
"fmt"
"io/fs"
"path/filepath"
"strings"
"github.com/block/opencli-go"
"github.com/square/exit"
)
// OpenCLIContract handles executables that respond to --help-opencli
// with an OpenCLI document (github.com/block/opencli-go).
//
// Unlike ExecutableContract, this contract does not require any particular
// file extension. Any executable file is eligible.
type OpenCLIContract struct{}
// OpenCLIDescriber is implemented by Commands that can describe themselves
// using the OpenCLI model (github.com/block/opencli-go). Not every Command
// implements it; reflect on a discovered Command to obtain its metadata:
//
// if d, ok := cmd.(OpenCLIDescriber); ok {
// if c, _ := d.OpenCLICommand(); c != nil {
// // c.Arguments, c.Options, c.Description, c.Examples, ...
// }
// }
//
// OpenCLICommand returns nil when the command has no OpenCLI metadata (for
// example, a command discovered via a contract other than OpenCLI).
//
// The returned Command describes only this node; its Commands field is not
// populated. Walk Subcommands() to describe the command tree.
type OpenCLIDescriber interface {
OpenCLICommand() (*opencli.Command, error)
}
func (c *OpenCLIContract) BuildCommand(path string, info fs.DirEntry, parent Command, d DiscoveryContext) (Command, error) {
if info.IsDir() {
return nil, ErrNotApplicable
}
if ok, err := isExecutable(info); err != nil {
return nil, err
} else if !ok {
return nil, ErrNotApplicable
}
name := filepath.Base(path)
if d.MaxDepth() == 0 {
return &executableCommand{
parent: parent,
path: path,
name: name,
discoveredIn: filepath.Dir(path),
executor: d.Executor(),
cache: d.Cache(),
contract: "OpenCLI",
}, nil
}
return &executableCommand{
parent: parent,
path: path,
name: name,
discoveredIn: filepath.Dir(path),
executor: d.Executor(),
cache: d.Cache(),
discoverer: d.Next(),
describe: describeOpenCLI,
contract: "OpenCLI",
}, nil
}
// describeOpenCLI is a describeFunc that invokes --help-opencli
// and parses the OpenCLI JSON output into a commandDescriptor.
func describeOpenCLI(cmd *executableCommand) (*commandDescriptor, error) {
out, err := cmd.cache.Fetch(cmd, "help-opencli", func() (string, error) {
return helpOpenCLIRaw(cmd)
})
if err != nil {
return nil, err
}
return parseOpenCLI(cmd, out)
}
// helpOpenCLIRaw executes --help-opencli and returns the raw JSON output.
func helpOpenCLIRaw(m *executableCommand) (string, error) {
cmd := m.Command("--help-opencli")
out, err := m.output(cmd)
if err != nil {
err = fmt.Errorf("exec '%s': %w", strings.Join(cmd.Args, " "), err)
return "", exit.Wrap(
CommandDescribeError{
CommandError{
Message: err.Error(),
Command: m,
Cause: err,
},
},
exit.InternalError,
)
}
return string(out), nil
}
// parseOpenCLI parses OpenCLI JSON output into a commandDescriptor.
func parseOpenCLI(cmd *executableCommand, out string) (*commandDescriptor, error) {
var doc opencli.Document
if err := json.Unmarshal([]byte(out), &doc); err != nil {
return &commandDescriptor{},
exit.Wrap(
CommandDescribeError{
CommandError{
Message: fmt.Sprintf("error parsing output from `%s --help-opencli`: %s", cmd.path, err),
Command: cmd,
Cause: err,
},
},
exit.InternalError,
)
}
return opencliToDescriptor(doc.Command), nil
}
// opencliToDescriptor converts an opencli.Command into a commandDescriptor,
// mapping the fields that Exoskeleton uses and ignoring the rest.
func opencliToDescriptor(cmd opencli.Command) *commandDescriptor {
d := &commandDescriptor{
Name: cmd.Name,
Summary: cmd.Summary,
Aliases: cmd.Aliases,
}
if cmd.DefaultCommand != nil {
d.DefaultCommand = *cmd.DefaultCommand
}
// Retain the node's OpenCLI metadata so it can be surfaced via
// OpenCLIDescriber. Strip Commands: children are represented by
// d.Commands and reachable through Subcommands().
nodeLocal := cmd
nodeLocal.Commands = nil
d.openCLI = &nodeLocal
if len(cmd.Commands) > 0 {
d.Commands = make([]*commandDescriptor, len(cmd.Commands))
for i, sub := range cmd.Commands {
d.Commands[i] = opencliToDescriptor(sub)
}
}
return d
}