package cmd import ( "fmt" "log" "net/http" "os" "path/filepath" "strings" "github.com/spf13/cobra" "github.com/insertr/cli/pkg/content" ) var servedevCmd = &cobra.Command{ Use: "servedev", Short: "Development server that parses and serves enhanced HTML files", Long: `Servedev starts a development HTTP server that automatically parses HTML files for insertr elements and serves the enhanced content. Perfect for development workflow with live rebuilds via Air.`, Run: runServedev, } var ( inputDir string port int useMockContent bool devSiteID string ) func init() { rootCmd.AddCommand(servedevCmd) servedevCmd.Flags().StringVarP(&inputDir, "input", "i", ".", "Input directory to serve") servedevCmd.Flags().IntVarP(&port, "port", "p", 3000, "Port to serve on") servedevCmd.Flags().BoolVar(&useMockContent, "mock", true, "Use mock content for development") servedevCmd.Flags().StringVarP(&devSiteID, "site-id", "s", "demo", "Site ID for content lookup") } func runServedev(cmd *cobra.Command, args []string) { // Resolve absolute path for input directory absInputDir, err := filepath.Abs(inputDir) if err != nil { log.Fatalf("Error resolving input directory: %v", err) } // Check if input directory exists if _, err := os.Stat(absInputDir); os.IsNotExist(err) { log.Fatalf("Input directory does not exist: %s", absInputDir) } // Create content client var client content.ContentClient if useMockContent { fmt.Printf("๐Ÿงช Using mock content for development\n") client = content.NewMockClient() } else { // For now, default to mock if no API URL provided fmt.Printf("๐Ÿงช Using mock content for development (no API configured)\n") client = content.NewMockClient() } fmt.Printf("๐Ÿš€ Starting development server with content enhancement...\n") fmt.Printf("๐Ÿ“ Serving directory: %s\n", absInputDir) fmt.Printf("๐ŸŒ Server running at: http://localhost:%d\n", port) fmt.Printf("๐Ÿท๏ธ Site ID: %s\n", devSiteID) fmt.Printf("๐Ÿ”„ Manually refresh browser to see changes\n\n") // Create enhanced file server fileServer := http.FileServer(&enhancedFileSystem{ fs: http.Dir(absInputDir), dir: absInputDir, enhancer: content.NewEnhancer(client, devSiteID), }) // Handle editor assets http.HandleFunc("/_insertr/", func(w http.ResponseWriter, r *http.Request) { assetPath := strings.TrimPrefix(r.URL.Path, "/_insertr/") serveEditorAsset(w, r, assetPath) }) // Handle insertr library files http.HandleFunc("/insertr/", func(w http.ResponseWriter, r *http.Request) { assetPath := strings.TrimPrefix(r.URL.Path, "/insertr/") serveLibraryAsset(w, r, assetPath) }) // Handle all other requests with our enhanced file server http.Handle("/", fileServer) // Start server addr := fmt.Sprintf(":%d", port) log.Fatal(http.ListenAndServe(addr, nil)) } // serveEditorAsset serves editor JavaScript and CSS files func serveEditorAsset(w http.ResponseWriter, r *http.Request, assetPath string) { // Get the path to the CLI binary directory execPath, err := os.Executable() if err != nil { http.NotFound(w, r) return } // Look for assets relative to the CLI binary (for built version) assetsDir := filepath.Join(filepath.Dir(execPath), "assets", "editor") assetFile := filepath.Join(assetsDir, assetPath) // If not found, look for assets relative to source (for development) if _, err := os.Stat(assetFile); os.IsNotExist(err) { // Assume we're running from source cwd, _ := os.Getwd() assetsDir = filepath.Join(cwd, "assets", "editor") assetFile = filepath.Join(assetsDir, assetPath) } // Set appropriate content type if strings.HasSuffix(assetPath, ".js") { w.Header().Set("Content-Type", "application/javascript") } else if strings.HasSuffix(assetPath, ".css") { w.Header().Set("Content-Type", "text/css") } // Serve the file http.ServeFile(w, r, assetFile) } // serveLibraryAsset serves the insertr library files from embedded assets func serveLibraryAsset(w http.ResponseWriter, r *http.Request, assetPath string) { w.Header().Set("Content-Type", "application/javascript") var script string switch assetPath { case "insertr.js": script = content.GetLibraryScript(false) case "insertr.min.js": script = content.GetLibraryScript(true) default: http.NotFound(w, r) return } w.Write([]byte(script)) } // enhancedFileSystem wraps http.FileSystem to provide enhanced HTML serving type enhancedFileSystem struct { fs http.FileSystem dir string enhancer *content.Enhancer } func (efs *enhancedFileSystem) Open(name string) (http.File, error) { // For HTML files, enhance them on-the-fly if strings.HasSuffix(name, ".html") { fmt.Printf("๐Ÿ“„ Enhancing HTML: %s\n", name) return efs.serveEnhancedHTML(name) } // For non-HTML files, serve as-is return efs.fs.Open(name) } // serveEnhancedHTML enhances an HTML file and returns it as an http.File func (efs *enhancedFileSystem) serveEnhancedHTML(name string) (http.File, error) { // Get the full file path inputPath := filepath.Join(efs.dir, name) // Create a temporary output path (in-memory would be better, but this is simpler for now) tempDir := filepath.Join(os.TempDir(), "insertr-dev") outputPath := filepath.Join(tempDir, name) // Ensure temp directory exists if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil { fmt.Printf("โš ๏ธ Failed to create temp directory: %v\n", err) return efs.fs.Open(name) // Fallback to original file } // Enhance the file if err := efs.enhancer.EnhanceFile(inputPath, outputPath); err != nil { fmt.Printf("โš ๏ธ Enhancement failed for %s: %v\n", name, err) return efs.fs.Open(name) // Fallback to original file } // Serve the enhanced file tempFS := http.Dir(tempDir) return tempFS.Open(name) }