Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/cheatmd/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ var chainResetCmd = &cobra.Command{

func runChainReset(cmd *cobra.Command, args []string) error {
root := "."
if config.GetPath() != "." {
root = config.GetPath()
if config.Get().Path != "." {
root = config.Get().Path
}
absRoot, err := filepath.Abs(root)
if err != nil {
Expand Down
122 changes: 73 additions & 49 deletions cmd/cheatmd/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,39 +32,70 @@ func init() {
composeCmd.Flags().BoolP("print", "p", false, "Print to stdout instead of saving to a file")
}

func runCompose(cmd *cobra.Command, args []string) error {
type composeOptions struct {
name string
desc string
file string
printOnly bool
}

func parseComposeOptions(cmd *cobra.Command) composeOptions {
name, _ := cmd.Flags().GetString("name")
desc, _ := cmd.Flags().GetString("description")
file, _ := cmd.Flags().GetString("file")
printOnly, _ := cmd.Flags().GetBool("print")
return composeOptions{name, desc, file, printOnly}
}

func runCompose(cmd *cobra.Command, args []string) error {
opts := parseComposeOptions(cmd)

command, err := readComposeCommand(args)
if err != nil {
return err
}

outStr := buildComposeMarkdown(opts.name, opts.desc, command)

if opts.printOnly {
fmt.Fprint(cmd.OutOrStdout(), strings.TrimPrefix(outStr, "\n"))
return nil
}

targetFile, err := determineComposeTargetFile(opts.file)
if err != nil {
return err
}

return appendComposeToFile(targetFile, outStr, cmd.ErrOrStderr(), opts.name)
}

var command string
func readComposeCommand(args []string) (string, error) {
if len(args) > 0 {
command = strings.Join(args, " ")
} else {
// Read from stdin
stat, err := os.Stdin.Stat()
if err != nil {
return err
}
if (stat.Mode() & os.ModeCharDevice) != 0 {
return fmt.Errorf("no command provided and no input piped. Use `cheatmd compose \"command\"`")
}
b, err := io.ReadAll(os.Stdin)
if err != nil {
return err
}
command = strings.TrimSpace(string(b))
return strings.Join(args, " "), nil
}

stat, err := os.Stdin.Stat()
if err != nil {
return "", err
}
if (stat.Mode() & os.ModeCharDevice) != 0 {
return "", fmt.Errorf("no command provided and no input piped. Use `cheatmd compose \"command\"`")
}
b, err := io.ReadAll(os.Stdin)
if err != nil {
return "", err
}
command := strings.TrimSpace(string(b))
if command == "" {
return fmt.Errorf("command cannot be empty")
return "", fmt.Errorf("command cannot be empty")
}
return command, nil
}

// Extract variables
func buildComposeMarkdown(name, desc, command string) string {
vars := parser.ExtractVars(command)

// Build markdown
var sb strings.Builder
sb.WriteString("\n# ")
sb.WriteString(name)
Expand All @@ -87,45 +118,38 @@ func runCompose(cmd *cobra.Command, args []string) error {
sb.WriteString(command)
sb.WriteString("\n```\n")

outStr := sb.String()
return sb.String()
}

if printOnly {
fmt.Fprint(cmd.OutOrStdout(), strings.TrimPrefix(outStr, "\n"))
return nil
func determineComposeTargetFile(file string) (string, error) {
if file != "" {
return file, nil
}

// Determine output file
targetFile := file
if targetFile == "" {
// Use default
cfgPath := config.GetPath()
if cfgPath == "" {
return fmt.Errorf("no cheat path configured, please use -f or set cheatmd path")
}
firstPath := strings.Split(cfgPath, ",")[0]
stat, err := os.Stat(firstPath)
if err != nil {
// Path does not exist, assume it's a directory we should create?
// To be safe, let's check if it ends in .md
if strings.HasSuffix(strings.ToLower(firstPath), ".md") {
targetFile = firstPath
} else {
targetFile = filepath.Join(firstPath, "snippets.md")
}
} else if stat.IsDir() {
targetFile = filepath.Join(firstPath, "snippets.md")
} else {
targetFile = firstPath
cfgPath := config.Get().Path
if cfgPath == "" {
return "", fmt.Errorf("no cheat path configured, please use -f or set cheatmd path")
}
firstPath := strings.Split(cfgPath, ",")[0]
stat, err := os.Stat(firstPath)
if err != nil {
if strings.HasSuffix(strings.ToLower(firstPath), ".md") {
return firstPath, nil
}
return filepath.Join(firstPath, "snippets.md"), nil
}
if stat.IsDir() {
return filepath.Join(firstPath, "snippets.md"), nil
}
return firstPath, nil
}

// Ensure directory exists
func appendComposeToFile(targetFile, outStr string, stderr io.Writer, name string) error {
targetDir := filepath.Dir(targetFile)
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("failed to create directory for snippets: %w", err)
}

// Append to file
f, err := os.OpenFile(targetFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", targetFile, err)
Expand All @@ -136,6 +160,6 @@ func runCompose(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to write to file %s: %w", targetFile, err)
}

fmt.Fprintf(cmd.ErrOrStderr(), "✓ Cheat '%s' appended to %s\n", name, targetFile)
fmt.Fprintf(stderr, "✓ Cheat '%s' appended to %s\n", name, targetFile)
return nil
}
210 changes: 3 additions & 207 deletions cmd/cheatmd/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,213 +59,9 @@ func runConvert(cmd *cobra.Command, args []string) error {
// so we need to parse every .cheat file into a shared index before
// emitting any of them. The other formats are still per-file.
if format == "navi" {
return convertNaviDirectory(inputAbs, outputPath)
return convert.ConvertNaviDirectory(inputAbs, outputPath)
}
return convertDirectory(format, inputAbs, outputPath)
return convert.ConvertDirectory(format, inputAbs, outputPath)
}
return convertFile(format, inputAbs, outputPath)
}

// convertNaviDirectory walks every .cheat file in inputDir, parses them all
// into a shared NaviIndex (so @extends references can resolve across files),
// and writes one converted markdown file per source under outputDir.
func convertNaviDirectory(inputDir, outputDir string) error {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("failed to create output directory %s: %w", outputDir, err)
}

sources, rels, err := collectNaviSources(inputDir)
if err != nil {
return err
}
if len(sources) == 0 {
return nil
}

results := convert.ConvertNaviTree(sources)
for i, res := range results {
rel := rels[i]
relBase := strings.TrimSuffix(rel, filepath.Ext(rel))
targetFile := filepath.Join(outputDir, relBase+".md")
if err := os.MkdirAll(filepath.Dir(targetFile), 0755); err != nil {
return fmt.Errorf("failed to create directory for %s: %w", targetFile, err)
}
if err := os.WriteFile(targetFile, []byte(res.Content), 0644); err != nil {
return fmt.Errorf("failed to write converted file %s: %w", targetFile, err)
}
fmt.Printf("✓ Converted %s (navi) -> %s\n", rel, targetFile)
}
return nil
}

// collectNaviSources walks inputDir, returning every .cheat file's content
// alongside its relative path so the writer can preserve directory structure.
func collectNaviSources(inputDir string) ([]convert.NaviSource, []string, error) {
var sources []convert.NaviSource
var rels []string

err := filepath.WalkDir(inputDir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if strings.HasPrefix(d.Name(), ".") && d.Name() != "." {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(strings.ToLower(d.Name()), ".cheat") {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", path, err)
}
rel, err := filepath.Rel(inputDir, path)
if err != nil {
return err
}
sources = append(sources, convert.NaviSource{Path: path, Content: string(data)})
rels = append(rels, rel)
return nil
})
if err != nil {
return nil, nil, fmt.Errorf("error walking directory: %w", err)
}
return sources, rels, nil
}

func convertFile(format, inputPath, outputPath string) error {
data, err := os.ReadFile(inputPath)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", inputPath, err)
}

var converted string
switch format {
case "navi":
converted, err = convert.ConvertNavi(string(data), inputPath)
case "tldr":
converted, err = convert.ConvertTldr(string(data), inputPath)
case "cheat":
converted, err = convert.ConvertCheat(string(data), inputPath)
}

if err != nil {
return fmt.Errorf("conversion failed: %w", err)
}

targetFile := outputPath
outInfo, err := os.Stat(outputPath)
if err == nil && outInfo.IsDir() {
// Output is an existing directory, save as <basename>.md
base := filepath.Base(inputPath)
base = strings.TrimSuffix(base, filepath.Ext(base))
targetFile = filepath.Join(outputPath, base+".md")
} else if err != nil && (strings.HasSuffix(outputPath, "/") || strings.HasSuffix(outputPath, "\\")) {
// Output doesn't exist but looks like a directory path
if err := os.MkdirAll(outputPath, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", outputPath, err)
}
base := filepath.Base(inputPath)
base = strings.TrimSuffix(base, filepath.Ext(base))
targetFile = filepath.Join(outputPath, base+".md")
} else {
// Output is a file path, ensure directory exists
dir := filepath.Dir(outputPath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
}

if err := os.WriteFile(targetFile, []byte(converted), 0644); err != nil {
return fmt.Errorf("failed to write output to %s: %w", targetFile, err)
}

fmt.Printf("✓ Converted %s (%s) -> %s\n", filepath.Base(inputPath), format, targetFile)
return nil
}

func convertDirectory(format, inputDir, outputDir string) error {
// Create output dir if it doesn't exist
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("failed to create output directory %s: %w", outputDir, err)
}

err := filepath.WalkDir(inputDir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}

if d.IsDir() {
// Skip hidden dirs
if strings.HasPrefix(d.Name(), ".") && d.Name() != "." {
return filepath.SkipDir
}
return nil
}

// Check if the file matches our format's expected extension/criteria
shouldConvert := false
switch format {
case "navi":
shouldConvert = strings.HasSuffix(strings.ToLower(d.Name()), ".cheat")
case "tldr":
shouldConvert = strings.HasSuffix(strings.ToLower(d.Name()), ".md")
case "cheat":
// cheat/cheat community files typically have no extension and aren't hidden
shouldConvert = !strings.Contains(d.Name(), ".") && !strings.HasPrefix(d.Name(), "_")
}

if !shouldConvert {
return nil
}

// Calculate relative path to maintain structure
rel, err := filepath.Rel(inputDir, path)
if err != nil {
return err
}

relBase := strings.TrimSuffix(rel, filepath.Ext(rel))
targetFile := filepath.Join(outputDir, relBase+".md")

data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", path, err)
}

var converted string
switch format {
case "navi":
converted, err = convert.ConvertNavi(string(data), path)
case "tldr":
converted, err = convert.ConvertTldr(string(data), path)
case "cheat":
converted, err = convert.ConvertCheat(string(data), path)
}

if err != nil {
return fmt.Errorf("failed to convert file %s: %w", path, err)
}

// Ensure parent directory exists for the output file
parentDir := filepath.Dir(targetFile)
if err := os.MkdirAll(parentDir, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", parentDir, err)
}

if err := os.WriteFile(targetFile, []byte(converted), 0644); err != nil {
return fmt.Errorf("failed to write converted file %s: %w", targetFile, err)
}

fmt.Printf("✓ Converted %s (%s) -> %s\n", rel, format, targetFile)
return nil
})

if err != nil {
return fmt.Errorf("error walking directory: %w", err)
}

return nil
return convert.ConvertFile(format, inputAbs, outputPath)
}
Loading
Loading