Skip to content

Commit 986894d

Browse files
spboyerCopilot
andcommitted
feat: add FileWriter for safe scaffold file creation
Closes #48 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent fd72379 commit 986894d

4 files changed

Lines changed: 369 additions & 39 deletions

File tree

cmd/waza/cmd_init.go

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -512,33 +512,47 @@ func initCommandE(cmd *cobra.Command, args []string, noSkill bool, flagSkillsDir
512512

513513
fmt.Fprintf(out, "\nProject structure:\n\n") //nolint:errcheck
514514

515+
// Handle directories first
516+
for _, item := range items {
517+
if item.isDir {
518+
if err := os.MkdirAll(item.path, 0o755); err != nil {
519+
return fmt.Errorf("failed to create %s: %w", item.path, err)
520+
}
521+
}
522+
}
523+
524+
// Prepare file entries for FileWriter
525+
var fileEntries []scaffold.FileEntry
526+
for _, item := range items {
527+
if !item.isDir && item.content != "" {
528+
fileEntries = append(fileEntries, scaffold.FileEntry{
529+
Path: item.path,
530+
Content: item.content,
531+
Label: item.label,
532+
})
533+
}
534+
}
535+
536+
// Write files using FileWriter
537+
writer := scaffold.NewFileWriter(fileEntries)
538+
fileInventory, err := writer.Write()
539+
if err != nil {
540+
return err
541+
}
542+
543+
// Build outcome map for files
544+
fileOutcomes := make(map[string]scaffold.FileOutcome)
545+
for _, inv := range fileInventory {
546+
fileOutcomes[inv.Path] = inv.Outcome
547+
}
548+
549+
// Display inventory
515550
var buf2 bytes.Buffer
516551
tw2 := tabwriter.NewWriter(&buf2, 0, 0, 2, ' ', 0)
517552
created := 0
553+
518554
for _, item := range items {
519555
var indicator string
520-
var existed bool
521-
522-
if item.isDir {
523-
if info, err := os.Stat(item.path); err == nil && info.IsDir() {
524-
existed = true
525-
} else {
526-
if err := os.MkdirAll(item.path, 0o755); err != nil {
527-
return fmt.Errorf("failed to create %s: %w", item.path, err)
528-
}
529-
}
530-
} else {
531-
if _, err := os.Stat(item.path); err == nil {
532-
existed = true
533-
} else if item.content != "" {
534-
if err := os.MkdirAll(filepath.Dir(item.path), 0o755); err != nil {
535-
return fmt.Errorf("failed to create directory for %s: %w", item.path, err)
536-
}
537-
if err := os.WriteFile(item.path, []byte(item.content), 0o644); err != nil {
538-
return fmt.Errorf("failed to write %s: %w", item.path, err)
539-
}
540-
}
541-
}
542556

543557
// Relative path for display
544558
relPath := item.path
@@ -549,11 +563,19 @@ func initCommandE(cmd *cobra.Command, args []string, noSkill bool, flagSkillsDir
549563
relPath += string(filepath.Separator)
550564
}
551565

552-
if existed {
566+
if item.isDir {
567+
// Directories always show as existing (we created them if needed above)
553568
indicator = "{exist}"
554-
} else {
555-
indicator = "{new}"
556-
created++
569+
} else if item.content != "" {
570+
// File - check outcome from FileWriter
571+
if outcome, ok := fileOutcomes[item.path]; ok {
572+
if outcome == scaffold.FileCreated {
573+
indicator = "{new}"
574+
created++
575+
} else {
576+
indicator = "{exist}"
577+
}
578+
}
557579
}
558580

559581
fmt.Fprintf(tw2, " %s\t%s\t%s\n", indicator, relPath, item.label) //nolint:errcheck
@@ -568,7 +590,7 @@ func initCommandE(cmd *cobra.Command, args []string, noSkill bool, flagSkillsDir
568590
fmt.Fprintln(out) //nolint:errcheck
569591
if created == 0 {
570592
fmt.Fprintf(out, "✅ Project up to date.\n") //nolint:errcheck
571-
} else if created == len(items) {
593+
} else if created == len(fileEntries) {
572594
fmt.Fprintf(out, "✅ Project created — %d items set up.\n", created) //nolint:errcheck
573595
} else {
574596
fmt.Fprintf(out, "✅ Repaired — %d item(s) added.\n", created) //nolint:errcheck

cmd/waza/cmd_new.go

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,39 @@ func writeFiles(cmd *cobra.Command, files []fileEntry, skillName string, existin
337337
fmt.Fprintf(cmd.OutOrStdout(), "\nSkill structure:\n\n") //nolint:errcheck
338338

339339
baseDir, _ := os.Getwd() //nolint:errcheck // best-effort for display paths
340+
341+
// Separate files into overwrite and normal entries
342+
var normalEntries []scaffold.FileEntry
343+
var overwriteFiles []fileEntry
344+
345+
for _, f := range files {
346+
if f.overwrite {
347+
overwriteFiles = append(overwriteFiles, f)
348+
} else {
349+
normalEntries = append(normalEntries, scaffold.FileEntry{
350+
Path: f.path,
351+
Content: f.content,
352+
Label: f.label,
353+
})
354+
}
355+
}
356+
357+
// Use FileWriter for normal files
358+
writer := scaffold.NewFileWriter(normalEntries)
359+
fileInventory, err := writer.Write()
360+
if err != nil {
361+
return err
362+
}
363+
364+
// Build outcome map
365+
fileOutcomes := make(map[string]scaffold.FileOutcome)
366+
for _, inv := range fileInventory {
367+
fileOutcomes[inv.Path] = inv.Outcome
368+
}
369+
340370
created := 0
371+
372+
// Display outcomes for all files
341373
for _, f := range files {
342374
relPath := f.path
343375
if baseDir != "" {
@@ -348,25 +380,26 @@ func writeFiles(cmd *cobra.Command, files []fileEntry, skillName string, existin
348380
}
349381
}
350382

351-
if _, err := os.Stat(f.path); err == nil {
352-
if f.overwrite {
353-
// Overwrite existing file (e.g., malformed SKILL.md being repaired)
383+
if f.overwrite {
384+
// Handle overwrite files (malformed SKILL.md repair)
385+
if _, statErr := os.Stat(f.path); statErr == nil {
354386
if err := os.WriteFile(f.path, []byte(f.content), 0o644); err != nil {
355387
return fmt.Errorf("failed to write %s: %w", f.path, err)
356388
}
357389
fmt.Fprintf(cmd.OutOrStdout(), " %s %-40s %s (updated)\n", yellowPlus, relPath, f.label) //nolint:errcheck
358390
created++
359-
continue
360391
}
361-
fmt.Fprintf(cmd.OutOrStdout(), " %s %-40s %s\n", greenCheck, relPath, f.label) //nolint:errcheck
362-
continue
363-
}
364-
365-
if err := os.WriteFile(f.path, []byte(f.content), 0o644); err != nil {
366-
return fmt.Errorf("failed to write %s: %w", f.path, err)
392+
} else {
393+
// Use outcome from FileWriter
394+
if outcome, ok := fileOutcomes[f.path]; ok {
395+
if outcome == scaffold.FileCreated {
396+
fmt.Fprintf(cmd.OutOrStdout(), " %s %-40s %s\n", yellowPlus, relPath, f.label) //nolint:errcheck
397+
created++
398+
} else {
399+
fmt.Fprintf(cmd.OutOrStdout(), " %s %-40s %s\n", greenCheck, relPath, f.label) //nolint:errcheck
400+
}
401+
}
367402
}
368-
fmt.Fprintf(cmd.OutOrStdout(), " %s %-40s %s\n", yellowPlus, relPath, f.label) //nolint:errcheck
369-
created++
370403
}
371404

372405
// Show summary lines for user-owned tasks/fixtures directories

internal/scaffold/writer.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package scaffold
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
)
8+
9+
// FileEntry represents a file to be written with its content and label.
10+
type FileEntry struct {
11+
Path string
12+
Content string
13+
Label string
14+
}
15+
16+
// FileOutcome describes what happened when writing a file.
17+
type FileOutcome string
18+
19+
const (
20+
FileCreated FileOutcome = "created"
21+
FileSkipped FileOutcome = "skipped"
22+
)
23+
24+
// FileInventory represents the result of writing a file.
25+
type FileInventory struct {
26+
Path string
27+
Label string
28+
Outcome FileOutcome
29+
}
30+
31+
// FileWriter handles safe file creation with existence checking.
32+
type FileWriter struct {
33+
entries []FileEntry
34+
}
35+
36+
// NewFileWriter creates a new FileWriter with the given file entries.
37+
func NewFileWriter(entries []FileEntry) *FileWriter {
38+
return &FileWriter{entries: entries}
39+
}
40+
41+
// Write writes all files, creating missing ones and skipping existing ones.
42+
// It creates parent directories as needed and returns an inventory of outcomes.
43+
func (w *FileWriter) Write() ([]FileInventory, error) {
44+
inventory := make([]FileInventory, 0, len(w.entries))
45+
46+
for _, entry := range w.entries {
47+
outcome, err := w.writeFile(entry)
48+
if err != nil {
49+
return inventory, err
50+
}
51+
52+
inventory = append(inventory, FileInventory{
53+
Path: entry.Path,
54+
Label: entry.Label,
55+
Outcome: outcome,
56+
})
57+
}
58+
59+
return inventory, nil
60+
}
61+
62+
// writeFile writes a single file, returning its outcome.
63+
func (w *FileWriter) writeFile(entry FileEntry) (FileOutcome, error) {
64+
// Check if file exists
65+
if _, err := os.Stat(entry.Path); err == nil {
66+
return FileSkipped, nil
67+
}
68+
69+
// Create parent directory if needed
70+
dir := filepath.Dir(entry.Path)
71+
if err := os.MkdirAll(dir, 0o755); err != nil {
72+
return "", fmt.Errorf("failed to create directory for %s: %w", entry.Path, err)
73+
}
74+
75+
// Write file
76+
if err := os.WriteFile(entry.Path, []byte(entry.Content), 0o644); err != nil {
77+
return "", fmt.Errorf("failed to write %s: %w", entry.Path, err)
78+
}
79+
80+
return FileCreated, nil
81+
}

0 commit comments

Comments
 (0)