Skip to content

Commit 8bf725d

Browse files
grokifyclaude
andcommitted
feat(cli): add pidl command-line tool
Add CLI tool with commands: - validate: Validate PIDL JSON files - generate: Generate diagrams (PlantUML, Mermaid, DOT) - examples: List and show built-in examples - init: Create new protocol files from templates Supports generating from example names directly and copying examples as starting points. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 66f6aa6 commit 8bf725d

1 file changed

Lines changed: 352 additions & 0 deletions

File tree

cmd/pidl/main.go

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
// Command pidl is the CLI tool for the Protocol Interaction Description Language.
2+
package main
3+
4+
import (
5+
"flag"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/grokify/pidl"
12+
"github.com/grokify/pidl/examples"
13+
"github.com/grokify/pidl/render"
14+
)
15+
16+
const version = "0.1.0"
17+
18+
func main() {
19+
if len(os.Args) < 2 {
20+
printUsage()
21+
os.Exit(1)
22+
}
23+
24+
switch os.Args[1] {
25+
case "validate":
26+
cmdValidate(os.Args[2:])
27+
case "generate", "gen":
28+
cmdGenerate(os.Args[2:])
29+
case "examples", "list-examples":
30+
cmdExamples(os.Args[2:])
31+
case "init":
32+
cmdInit(os.Args[2:])
33+
case "version", "--version", "-v":
34+
fmt.Printf("pidl version %s\n", version)
35+
case "help", "--help", "-h":
36+
printUsage()
37+
default:
38+
fmt.Fprintf(os.Stderr, "Unknown command: %s\n\n", os.Args[1])
39+
printUsage()
40+
os.Exit(1)
41+
}
42+
}
43+
44+
func printUsage() {
45+
fmt.Print(`pidl - Protocol Interaction Description Language CLI
46+
47+
Usage:
48+
pidl <command> [options] [arguments]
49+
50+
Commands:
51+
validate Validate PIDL JSON files
52+
generate Generate diagrams from PIDL files
53+
examples List or show built-in examples
54+
init Create a new PIDL file from template
55+
version Print version information
56+
help Show this help message
57+
58+
Run 'pidl <command> -h' for more information on a command.
59+
`)
60+
}
61+
62+
func cmdValidate(args []string) {
63+
fs := flag.NewFlagSet("validate", flag.ExitOnError)
64+
quiet := fs.Bool("q", false, "Quiet mode (only show errors)")
65+
fs.Usage = func() {
66+
fmt.Print(`Usage: pidl validate [options] <file> [file...]
67+
68+
Validate PIDL JSON files against the schema.
69+
70+
Options:
71+
`)
72+
fs.PrintDefaults()
73+
}
74+
75+
if err := fs.Parse(args); err != nil {
76+
os.Exit(1)
77+
}
78+
79+
if fs.NArg() == 0 {
80+
fs.Usage()
81+
os.Exit(1)
82+
}
83+
84+
files := fs.Args()
85+
results := pidl.ValidateFiles(files)
86+
87+
hasErrors := false
88+
for _, r := range results {
89+
if r.ParseErr != nil {
90+
fmt.Fprintf(os.Stderr, "%s: parse error: %v\n", r.Filename, r.ParseErr)
91+
hasErrors = true
92+
continue
93+
}
94+
95+
if r.Errors.HasErrors() {
96+
fmt.Fprintf(os.Stderr, "%s: validation failed\n", r.Filename)
97+
for _, e := range r.Errors {
98+
fmt.Fprintf(os.Stderr, " - %s\n", e)
99+
}
100+
hasErrors = true
101+
continue
102+
}
103+
104+
if !*quiet {
105+
fmt.Printf("%s: valid (%s)\n", r.Filename, r.Protocol.ProtocolMeta.Name)
106+
}
107+
}
108+
109+
if hasErrors {
110+
os.Exit(1)
111+
}
112+
}
113+
114+
func cmdGenerate(args []string) {
115+
fs := flag.NewFlagSet("generate", flag.ExitOnError)
116+
formatStr := fs.String("f", "plantuml", "Output format: plantuml, mermaid, dot")
117+
output := fs.String("o", "", "Output file (default: stdout)")
118+
fs.Usage = func() {
119+
fmt.Print(`Usage: pidl generate [options] <file>
120+
121+
Generate diagram output from a PIDL file.
122+
123+
Options:
124+
`)
125+
fs.PrintDefaults()
126+
fmt.Print(`
127+
Formats:
128+
plantuml, puml PlantUML sequence diagram
129+
mermaid, mmd Mermaid sequence diagram
130+
dot, graphviz Graphviz DOT data flow diagram
131+
`)
132+
}
133+
134+
if err := fs.Parse(args); err != nil {
135+
os.Exit(1)
136+
}
137+
138+
if fs.NArg() == 0 {
139+
fs.Usage()
140+
os.Exit(1)
141+
}
142+
143+
format, err := render.ParseFormat(*formatStr)
144+
if err != nil {
145+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
146+
os.Exit(1)
147+
}
148+
149+
filename := fs.Arg(0)
150+
151+
// Check if it's an example name
152+
var p *pidl.Protocol
153+
if !strings.Contains(filename, "/") && !strings.Contains(filename, "\\") && !strings.HasSuffix(filename, ".json") {
154+
p, err = examples.GetProtocol(filename)
155+
if err != nil {
156+
// Try as file
157+
p, err = pidl.ParseFile(filename)
158+
}
159+
} else {
160+
p, err = pidl.ParseFile(filename)
161+
}
162+
163+
if err != nil {
164+
fmt.Fprintf(os.Stderr, "Error parsing %s: %v\n", filename, err)
165+
os.Exit(1)
166+
}
167+
168+
if errs := p.Validate(); errs.HasErrors() {
169+
fmt.Fprintf(os.Stderr, "Validation errors in %s:\n%s", filename, errs)
170+
os.Exit(1)
171+
}
172+
173+
diagram, err := render.RenderString(format, p)
174+
if err != nil {
175+
fmt.Fprintf(os.Stderr, "Error rendering: %v\n", err)
176+
os.Exit(1)
177+
}
178+
179+
if *output == "" {
180+
fmt.Print(diagram)
181+
} else {
182+
if err := os.WriteFile(*output, []byte(diagram), 0644); err != nil {
183+
fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", *output, err)
184+
os.Exit(1)
185+
}
186+
fmt.Printf("Wrote %s\n", *output)
187+
}
188+
}
189+
190+
func cmdExamples(args []string) {
191+
fs := flag.NewFlagSet("examples", flag.ExitOnError)
192+
showJSON := fs.Bool("json", false, "Show example JSON content")
193+
fs.Usage = func() {
194+
fmt.Print(`Usage: pidl examples [options] [name]
195+
196+
List built-in example protocols or show a specific example.
197+
198+
Options:
199+
`)
200+
fs.PrintDefaults()
201+
}
202+
203+
if err := fs.Parse(args); err != nil {
204+
os.Exit(1)
205+
}
206+
207+
if fs.NArg() == 0 {
208+
// List all examples
209+
names := examples.List()
210+
fmt.Println("Available examples:")
211+
for _, name := range names {
212+
ex, err := examples.Get(name)
213+
if err != nil {
214+
fmt.Printf(" %s\n", name)
215+
continue
216+
}
217+
p, err := ex.Protocol()
218+
if err != nil {
219+
fmt.Printf(" %s\n", name)
220+
continue
221+
}
222+
fmt.Printf(" %-30s %s\n", name, p.ProtocolMeta.Name)
223+
}
224+
fmt.Println("\nUse 'pidl examples <name>' to show details.")
225+
fmt.Println("Use 'pidl examples <name> -json' to show JSON content.")
226+
fmt.Println("Use 'pidl generate <name>' to generate diagrams.")
227+
return
228+
}
229+
230+
name := fs.Arg(0)
231+
232+
if *showJSON {
233+
data, err := examples.GetJSON(name)
234+
if err != nil {
235+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
236+
os.Exit(1)
237+
}
238+
fmt.Println(string(data))
239+
return
240+
}
241+
242+
ex, err := examples.Get(name)
243+
if err != nil {
244+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
245+
os.Exit(1)
246+
}
247+
248+
p, err := ex.Protocol()
249+
if err != nil {
250+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
251+
os.Exit(1)
252+
}
253+
254+
fmt.Printf("Example: %s\n", ex.Name)
255+
fmt.Printf("Protocol: %s\n", p.ProtocolMeta.Name)
256+
fmt.Printf("ID: %s\n", p.ProtocolMeta.ID)
257+
if p.ProtocolMeta.Description != "" {
258+
fmt.Printf("Description: %s\n", p.ProtocolMeta.Description)
259+
}
260+
fmt.Printf("Category: %s\n", p.ProtocolMeta.Category)
261+
fmt.Printf("Entities: %d\n", len(p.Entities))
262+
fmt.Printf("Phases: %d\n", len(p.Phases))
263+
fmt.Printf("Flows: %d\n", len(p.Flows))
264+
265+
if len(p.ProtocolMeta.References) > 0 {
266+
fmt.Println("References:")
267+
for _, ref := range p.ProtocolMeta.References {
268+
fmt.Printf(" - %s: %s\n", ref.Name, ref.URL)
269+
}
270+
}
271+
}
272+
273+
func cmdInit(args []string) {
274+
fs := flag.NewFlagSet("init", flag.ExitOnError)
275+
name := fs.String("name", "", "Protocol name")
276+
from := fs.String("from", "", "Initialize from example (e.g., oauth2_authorization_code)")
277+
fs.Usage = func() {
278+
fmt.Print(`Usage: pidl init [options] <filename>
279+
280+
Create a new PIDL file from a template or example.
281+
282+
Options:
283+
`)
284+
fs.PrintDefaults()
285+
fmt.Print(`
286+
Examples:
287+
pidl init my-protocol.json
288+
pidl init -name "My Protocol" my-protocol.json
289+
pidl init -from oauth2_authorization_code my-oauth.json
290+
`)
291+
}
292+
293+
if err := fs.Parse(args); err != nil {
294+
os.Exit(1)
295+
}
296+
297+
if fs.NArg() == 0 {
298+
fs.Usage()
299+
os.Exit(1)
300+
}
301+
302+
filename := fs.Arg(0)
303+
304+
// Check if file exists
305+
if _, err := os.Stat(filename); err == nil {
306+
fmt.Fprintf(os.Stderr, "Error: file already exists: %s\n", filename)
307+
os.Exit(1)
308+
}
309+
310+
var p *pidl.Protocol
311+
312+
if *from != "" {
313+
// Copy from example
314+
ex, err := examples.Get(*from)
315+
if err != nil {
316+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
317+
os.Exit(1)
318+
}
319+
p, err = ex.Protocol()
320+
if err != nil {
321+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
322+
os.Exit(1)
323+
}
324+
// Deep copy by serializing and deserializing
325+
data, _ := p.ToJSON()
326+
p, _ = pidl.Parse(data)
327+
} else {
328+
// Create minimal protocol
329+
id := pidl.SanitizeID(strings.TrimSuffix(filepath.Base(filename), ".json"))
330+
protocolName := *name
331+
if protocolName == "" {
332+
protocolName = strings.ReplaceAll(id, "_", " ")
333+
protocolName = strings.ReplaceAll(protocolName, "-", " ")
334+
protocolName = pidl.TitleCase(protocolName)
335+
}
336+
p = pidl.NewMinimalProtocol(id, protocolName)
337+
}
338+
339+
// Override name if provided
340+
if *name != "" {
341+
p.ProtocolMeta.Name = *name
342+
}
343+
344+
if err := pidl.WriteProtocolFile(filename, p); err != nil {
345+
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
346+
os.Exit(1)
347+
}
348+
349+
fmt.Printf("Created %s\n", filename)
350+
fmt.Printf("Protocol: %s\n", p.ProtocolMeta.Name)
351+
fmt.Printf("ID: %s\n", p.ProtocolMeta.ID)
352+
}

0 commit comments

Comments
 (0)