@@ -3,9 +3,11 @@ package main
33import (
44 "bytes"
55 "fmt"
6+ "io"
67 "os"
78 "os/exec"
89 "path/filepath"
10+ "runtime"
911 "strings"
1012
1113 "github.com/yuin/goldmark"
@@ -36,7 +38,7 @@ func convertMarkdownToHTML(markdown string) (string, error) {
3638 if err := md .Convert ([]byte (markdown ), & buf ); err != nil {
3739 return "" , fmt .Errorf ("markdown conversion failed: %w" , err )
3840 }
39-
41+
4042 // Trim trailing newline (goldmark adds it)
4143 html := strings .TrimSuffix (buf .String (), "\n " )
4244 return html , nil
@@ -50,17 +52,17 @@ func readAndConvertFile(path string) (string, error) {
5052 }
5153
5254 ext := strings .ToLower (filepath .Ext (path ))
53-
55+
5456 // If .html file, return as-is
5557 if ext == ".html" || ext == ".htm" {
5658 return string (content ), nil
5759 }
58-
60+
5961 // If .md file or no extension, convert Markdown to HTML
6062 if ext == ".md" || ext == "" {
6163 return convertMarkdownToHTML (string (content ))
6264 }
63-
65+
6466 // Unknown extension - assume Markdown for safety (agent-friendly default)
6567 return convertMarkdownToHTML (string (content ))
6668}
@@ -69,75 +71,182 @@ func readAndConvertFile(path string) (string, error) {
6971func processArgs (args []string ) ([]string , error ) {
7072 result := make ([]string , 0 , len (args ))
7173 i := 0
72-
74+
7375 for i < len (args ) {
7476 arg := args [i ]
75-
77+
7678 // Check for flags that need conversion
7779 if arg == "--description" || arg == "--body" {
7880 if i + 1 >= len (args ) {
7981 return nil , fmt .Errorf ("flag %s requires a value" , arg )
8082 }
81-
83+
8284 // Convert inline Markdown text to HTML
8385 markdown := args [i + 1 ]
8486 html , err := convertMarkdownToHTML (markdown )
8587 if err != nil {
8688 return nil , fmt .Errorf ("converting %s: %w" , arg , err )
8789 }
88-
90+
8991 result = append (result , arg , html )
9092 i += 2
9193 continue
9294 }
93-
95+
9496 if arg == "--description_file" || arg == "--body_file" {
9597 if i + 1 >= len (args ) {
9698 return nil , fmt .Errorf ("flag %s requires a file path" , arg )
9799 }
98-
100+
99101 // Read file and convert to HTML
100102 filePath := args [i + 1 ]
101103 html , err := readAndConvertFile (filePath )
102104 if err != nil {
103105 return nil , err
104106 }
105-
107+
106108 // Create temp file with HTML content
107109 tmpFile , err := os .CreateTemp ("" , "fizzy-md-*.html" )
108110 if err != nil {
109111 return nil , fmt .Errorf ("failed to create temp file: %w" , err )
110112 }
111113 defer tmpFile .Close ()
112-
114+
113115 if _ , err := tmpFile .WriteString (html ); err != nil {
114116 return nil , fmt .Errorf ("failed to write temp file: %w" , err )
115117 }
116-
118+
117119 // Replace flag with temp file path
118120 result = append (result , arg , tmpFile .Name ())
119121 i += 2
120122 continue
121123 }
122-
124+
123125 // Pass through all other arguments unchanged
124126 result = append (result , arg )
125127 i ++
126128 }
127-
129+
128130 return result , nil
129131}
130132
133+ // findFizzy locates the real fizzy binary, avoiding circular resolution.
134+ // Priority: FIZZY_PATH env var → PATH lookup (skipping our own binary).
135+ func findFizzy () string {
136+ // 1. Explicit override via env var
137+ if p := os .Getenv ("FIZZY_PATH" ); p != "" {
138+ if _ , err := os .Stat (p ); err == nil {
139+ return p
140+ }
141+ }
142+
143+ // 2. Resolve our own executable path so we can skip it
144+ self , _ := os .Executable ()
145+ if self != "" {
146+ self , _ = filepath .EvalSymlinks (self )
147+ }
148+
149+ // 3. Walk PATH entries looking for a "fizzy" that isn't us
150+ pathEnv := os .Getenv ("PATH" )
151+ for _ , dir := range filepath .SplitList (pathEnv ) {
152+ for _ , candidate := range fizzyCandidates (dir ) {
153+ info , err := os .Stat (candidate )
154+ if err != nil || info .IsDir () {
155+ continue
156+ }
157+
158+ real , _ := filepath .EvalSymlinks (candidate )
159+
160+ // Skip if it resolves to ourselves (fizzy-md)
161+ if self != "" && real == self {
162+ continue
163+ }
164+
165+ // Skip shell scripts that call fizzy-md (wrapper scripts)
166+ if isShellWrapperForFizzyMd (candidate ) {
167+ continue
168+ }
169+
170+ return candidate
171+ }
172+ }
173+
174+ return ""
175+ }
176+
177+ // isShellWrapperForFizzyMd does a quick check if a file is a small shell script
178+ // that references fizzy-md (i.e. a wrapper that would cause a loop).
179+ func isShellWrapperForFizzyMd (path string ) bool {
180+ info , err := os .Stat (path )
181+ if err != nil {
182+ return false
183+ }
184+ if info .IsDir () {
185+ return false
186+ }
187+ // Only check small files (real fizzy binary is >1MB)
188+ if info .Size () > 4096 {
189+ return false
190+ }
191+
192+ file , err := os .Open (path )
193+ if err != nil {
194+ return false
195+ }
196+ defer file .Close ()
197+
198+ buf := make ([]byte , 4096 )
199+ n , err := file .Read (buf )
200+ if err != nil && err != io .EOF {
201+ return false
202+ }
203+ content := string (buf [:n ])
204+ return strings .Contains (content , "fizzy-md" )
205+ }
206+
207+ func fizzyCandidates (dir string ) []string {
208+ base := "fizzy"
209+ if runtime .GOOS != "windows" {
210+ return []string {filepath .Join (dir , base )}
211+ }
212+ if filepath .Ext (base ) != "" {
213+ return []string {filepath .Join (dir , base )}
214+ }
215+
216+ pathext := os .Getenv ("PATHEXT" )
217+ var exts []string
218+ if pathext == "" {
219+ exts = []string {".com" , ".exe" , ".bat" , ".cmd" }
220+ } else {
221+ for _ , ext := range strings .Split (pathext , ";" ) {
222+ clean := strings .TrimSpace (ext )
223+ if clean == "" {
224+ continue
225+ }
226+ if ! strings .HasPrefix (clean , "." ) {
227+ clean = "." + clean
228+ }
229+ exts = append (exts , clean )
230+ }
231+ }
232+
233+ paths := make ([]string , 0 , len (exts ))
234+ for _ , ext := range exts {
235+ paths = append (paths , filepath .Join (dir , base + ext ))
236+ }
237+ return paths
238+ }
239+
131240func main () {
132241 // Get original args (skip program name)
133242 args := os .Args [1 :]
134-
243+
135244 // Handle --version flag
136245 if len (args ) == 1 && (args [0 ] == "--version" || args [0 ] == "-v" ) {
137246 fmt .Printf ("fizzy-md version %s\n " , version )
138247 os .Exit (0 )
139248 }
140-
249+
141250 // Handle stdin mode: if no args and stdin is piped, convert and output
142251 if len (args ) == 0 {
143252 stat , _ := os .Stdin .Stat ()
@@ -148,39 +257,39 @@ func main() {
148257 fmt .Fprintf (os .Stderr , "fizzy-md error: failed to read stdin: %v\n " , err )
149258 os .Exit (1 )
150259 }
151-
260+
152261 html , err := convertMarkdownToHTML (buf .String ())
153262 if err != nil {
154263 fmt .Fprintf (os .Stderr , "fizzy-md error: %v\n " , err )
155264 os .Exit (1 )
156265 }
157-
266+
158267 fmt .Print (html )
159268 os .Exit (0 )
160269 }
161270 }
162-
271+
163272 // Process args for Markdown conversion
164273 processedArgs , err := processArgs (args )
165274 if err != nil {
166275 fmt .Fprintf (os .Stderr , "fizzy-md error: %v\n " , err )
167276 os .Exit (1 )
168277 }
169-
170- // Find fizzy executable
171- fizzyPath , err := exec . LookPath ( "fizzy" )
172- if err != nil {
173- fmt .Fprintf (os .Stderr , "fizzy-md error: fizzy command not found in PATH \n " )
174- fmt .Fprintf (os .Stderr , "Please install fizzy-cli: https://github.com/robzolkos/fizzy-cli\n " )
278+
279+ // Find fizzy executable, avoiding circular resolution back to ourselves
280+ fizzyPath := findFizzy ( )
281+ if fizzyPath == "" {
282+ fmt .Fprintf (os .Stderr , "fizzy-md error: fizzy command not found\n " )
283+ fmt .Fprintf (os .Stderr , "Set FIZZY_PATH or install fizzy-cli: https://github.com/robzolkos/fizzy-cli\n " )
175284 os .Exit (1 )
176285 }
177-
286+
178287 // Execute real fizzy with processed args
179288 cmd := exec .Command (fizzyPath , processedArgs ... )
180289 cmd .Stdin = os .Stdin
181290 cmd .Stdout = os .Stdout
182291 cmd .Stderr = os .Stderr
183-
292+
184293 if err := cmd .Run (); err != nil {
185294 if exitErr , ok := err .(* exec.ExitError ); ok {
186295 os .Exit (exitErr .ExitCode ())
0 commit comments