cmd/sketch: add -dump-dist internal flag to extract embedded filesystem

Add internal CLI flag to dump embedded /dist/ filesystem contents to
a specified directory for debugging and development purposes.

Problem Analysis:
Developers and debugging scenarios sometimes need access to the built
embedded filesystem contents that are generated by webui.Build(). The
embedded filesystem is created dynamically and cached, making it difficult
to inspect or extract its contents for analysis, debugging, or external
use without complex workarounds.

Implementation Changes:

1. CLI Flag Addition:
   - Added -dump-dist string flag as internal/debugging option
   - Positioned in internal flags section alongside other developer tools
   - Flag accepts filesystem path as parameter for output directory
   - Implements early exit pattern after processing like -version and -list-models

2. Filesystem Extraction Function:
   - Created dumpDistFilesystem() function with comprehensive error handling
   - Uses webui.Build() to generate the embedded filesystem
   - Implements fs.WalkDir pattern for complete directory tree traversal
   - Handles both files and directories with appropriate permissions (0o755 for dirs, default for files)

3. File Copy Implementation:
   - Uses io.Copy for efficient file content transfer
   - Proper resource cleanup with defer statements for file handles
   - Preserves directory structure and file hierarchy
   - Creates output directory tree as needed with os.MkdirAll

4. Error Handling and User Feedback:
   - Comprehensive error messages for each failure point
   - Clear success message showing output directory path
   - Proper exit handling - program terminates after successful dump
   - Validates output directory creation before proceeding

Technical Details:
- Leverages existing webui.Build() infrastructure for filesystem generation
- Added required imports: io, io/fs for filesystem operations
- Uses filepath.Join for cross-platform path handling
- Implements early exit pattern consistent with other informational flags
- Positioned as internal flag visible only with -help-internal

Benefits:
- Enables inspection of generated webui assets for debugging
- Supports development workflows requiring filesystem analysis
- Provides clean extraction mechanism without modifying build process
- Useful for troubleshooting webui bundle generation issues
- Maintains separation as internal-only feature

Testing:
- Verified successful filesystem dump with proper directory structure
- Tested error handling with invalid output paths
- Confirmed program exits correctly after dump completion
- Validated flag appears in -help-internal output
- Tested with various output directory scenarios including existing/new paths

This addition provides essential debugging capability for webui filesystem
inspection while maintaining clean separation as an internal development tool.

Co-Authored-By: sketch <hello@sketch.dev>
Change-ID: s5fee523a25bc33dak
diff --git a/cmd/sketch/main.go b/cmd/sketch/main.go
index 2ec9699..4df1271 100644
--- a/cmd/sketch/main.go
+++ b/cmd/sketch/main.go
@@ -5,11 +5,14 @@
 	"context"
 	"flag"
 	"fmt"
+	"io"
+	"io/fs"
 	"log/slog"
 	"net"
 	"net/http"
 	"os"
 	"os/exec"
+	"path/filepath"
 	"runtime"
 	"runtime/debug"
 	"strings"
@@ -29,6 +32,7 @@
 	"sketch.dev/skabandclient"
 	"sketch.dev/skribe"
 	"sketch.dev/termui"
+	"sketch.dev/webui"
 
 	"golang.org/x/term"
 )
@@ -68,6 +72,10 @@
 		return nil
 	}
 
+	if flagArgs.dumpDist != "" {
+		return dumpDistFilesystem(flagArgs.dumpDist)
+	}
+
 	// Claude and Gemini are supported in container mode
 	// TODO: finish support--thread through API keys, add server support
 	isContainerSupported := flagArgs.modelName == "claude" || flagArgs.modelName == "" || flagArgs.modelName == "gemini"
@@ -184,6 +192,7 @@
 	verbose      bool
 	version      bool
 	workingDir   string
+	dumpDist     string
 	sshPort      int
 	forceRebuild bool
 
@@ -261,6 +270,9 @@
 	// This is really only useful for someone running with "go run"
 	userFlags.StringVar(&flags.workingDir, "C", "", "when set, change to this directory before running")
 
+	// Internal flags for development/debugging
+	internalFlags.StringVar(&flags.dumpDist, "dump-dist", "", "(internal) dump embedded /dist/ filesystem to specified directory and exit")
+
 	// Custom usage function that shows only user-visible flags by default
 	userFlags.Usage = func() {
 		fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
@@ -751,3 +763,59 @@
 		APIKey: apiKey,
 	}, nil
 }
+
+// dumpDistFilesystem dumps the embedded /dist/ filesystem to the specified directory
+func dumpDistFilesystem(outputDir string) error {
+	// Build the embedded filesystem
+	distFS, err := webui.Build()
+	if err != nil {
+		return fmt.Errorf("failed to build embedded filesystem: %w", err)
+	}
+
+	// Create the output directory
+	if err := os.MkdirAll(outputDir, 0o755); err != nil {
+		return fmt.Errorf("failed to create output directory %q: %w", outputDir, err)
+	}
+
+	// Walk through the filesystem and copy all files
+	err = fs.WalkDir(distFS, ".", func(path string, d fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+
+		outputPath := filepath.Join(outputDir, path)
+
+		if d.IsDir() {
+			// Create directory
+			if err := os.MkdirAll(outputPath, 0o755); err != nil {
+				return fmt.Errorf("failed to create directory %q: %w", outputPath, err)
+			}
+			return nil
+		}
+
+		// Copy file
+		src, err := distFS.Open(path)
+		if err != nil {
+			return fmt.Errorf("failed to open source file %q: %w", path, err)
+		}
+		defer src.Close()
+
+		dst, err := os.Create(outputPath)
+		if err != nil {
+			return fmt.Errorf("failed to create destination file %q: %w", outputPath, err)
+		}
+		defer dst.Close()
+
+		if _, err := io.Copy(dst, src); err != nil {
+			return fmt.Errorf("failed to copy file %q: %w", path, err)
+		}
+
+		return nil
+	})
+	if err != nil {
+		return fmt.Errorf("failed to dump filesystem: %w", err)
+	}
+
+	fmt.Printf("Successfully dumped embedded /dist/ filesystem to %q\n", outputDir)
+	return nil
+}