package deployments import ( "archive/tar" "bytes" "compress/gzip" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "os" "os/exec" "path/filepath" "strings" "github.com/DeBrosOfficial/network/pkg/auth" "github.com/spf13/cobra" ) // DeployCmd is the root deploy command var DeployCmd = &cobra.Command{ Use: "deploy", Short: "Deploy applications", Long: "Deploy static sites, Next.js apps, Go backends, and Node.js backends", } // DeployStaticCmd deploys a static site var DeployStaticCmd = &cobra.Command{ Use: "static ", Short: "Deploy a static site (React, Vue, etc.)", Args: cobra.ExactArgs(1), RunE: deployStatic, } // DeployNextJSCmd deploys a Next.js application var DeployNextJSCmd = &cobra.Command{ Use: "nextjs ", Short: "Deploy a Next.js application", Args: cobra.ExactArgs(1), RunE: deployNextJS, } // DeployGoCmd deploys a Go backend var DeployGoCmd = &cobra.Command{ Use: "go ", Short: "Deploy a Go backend", Args: cobra.ExactArgs(1), RunE: deployGo, } // DeployNodeJSCmd deploys a Node.js backend var DeployNodeJSCmd = &cobra.Command{ Use: "nodejs ", Short: "Deploy a Node.js backend", Args: cobra.ExactArgs(1), RunE: deployNodeJS, } var ( deployName string deploySubdomain string deploySSR bool deployUpdate bool ) func init() { DeployStaticCmd.Flags().StringVar(&deployName, "name", "", "Deployment name (required)") DeployStaticCmd.Flags().StringVar(&deploySubdomain, "subdomain", "", "Custom subdomain") DeployStaticCmd.Flags().BoolVar(&deployUpdate, "update", false, "Update existing deployment") DeployStaticCmd.MarkFlagRequired("name") DeployNextJSCmd.Flags().StringVar(&deployName, "name", "", "Deployment name (required)") DeployNextJSCmd.Flags().StringVar(&deploySubdomain, "subdomain", "", "Custom subdomain") DeployNextJSCmd.Flags().BoolVar(&deploySSR, "ssr", false, "Deploy with SSR (server-side rendering)") DeployNextJSCmd.Flags().BoolVar(&deployUpdate, "update", false, "Update existing deployment") DeployNextJSCmd.MarkFlagRequired("name") DeployGoCmd.Flags().StringVar(&deployName, "name", "", "Deployment name (required)") DeployGoCmd.Flags().StringVar(&deploySubdomain, "subdomain", "", "Custom subdomain") DeployGoCmd.Flags().BoolVar(&deployUpdate, "update", false, "Update existing deployment") DeployGoCmd.MarkFlagRequired("name") DeployNodeJSCmd.Flags().StringVar(&deployName, "name", "", "Deployment name (required)") DeployNodeJSCmd.Flags().StringVar(&deploySubdomain, "subdomain", "", "Custom subdomain") DeployNodeJSCmd.Flags().BoolVar(&deployUpdate, "update", false, "Update existing deployment") DeployNodeJSCmd.MarkFlagRequired("name") DeployCmd.AddCommand(DeployStaticCmd) DeployCmd.AddCommand(DeployNextJSCmd) DeployCmd.AddCommand(DeployGoCmd) DeployCmd.AddCommand(DeployNodeJSCmd) } func deployStatic(cmd *cobra.Command, args []string) error { sourcePath := args[0] // Warn if source looks like it needs building if _, err := os.Stat(filepath.Join(sourcePath, "package.json")); err == nil { if _, err := os.Stat(filepath.Join(sourcePath, "index.html")); os.IsNotExist(err) { fmt.Printf("āš ļø Warning: %s has package.json but no index.html. You may need to build first.\n", sourcePath) fmt.Printf(" Try: cd %s && npm run build, then deploy the output directory (e.g. dist/ or out/)\n\n", sourcePath) } } fmt.Printf("šŸ“¦ Creating tarball from %s...\n", sourcePath) tarball, err := createTarball(sourcePath) if err != nil { return fmt.Errorf("failed to create tarball: %w", err) } defer os.Remove(tarball) fmt.Printf("ā˜ļø Uploading to Orama Network...\n") endpoint := "/v1/deployments/static/upload" if deployUpdate { endpoint = "/v1/deployments/static/update" } resp, err := uploadDeployment(endpoint, tarball, map[string]string{ "name": deployName, "subdomain": deploySubdomain, }) if err != nil { return err } fmt.Printf("\nāœ… Deployment successful!\n\n") printDeploymentInfo(resp) return nil } func deployNextJS(cmd *cobra.Command, args []string) error { sourcePath, err := filepath.Abs(args[0]) if err != nil { return fmt.Errorf("failed to resolve path: %w", err) } // Verify it's a Next.js project if _, err := os.Stat(filepath.Join(sourcePath, "package.json")); os.IsNotExist(err) { return fmt.Errorf("no package.json found in %s", sourcePath) } // Step 1: Install dependencies if needed if _, err := os.Stat(filepath.Join(sourcePath, "node_modules")); os.IsNotExist(err) { fmt.Printf("šŸ“¦ Installing dependencies...\n") if err := runBuildCommand(sourcePath, "npm", "install"); err != nil { return fmt.Errorf("npm install failed: %w", err) } } // Step 2: Build fmt.Printf("šŸ”Ø Building Next.js application...\n") if err := runBuildCommand(sourcePath, "npm", "run", "build"); err != nil { return fmt.Errorf("build failed: %w", err) } var tarball string if deploySSR { // SSR: tarball the standalone output standalonePath := filepath.Join(sourcePath, ".next", "standalone") if _, err := os.Stat(standalonePath); os.IsNotExist(err) { return fmt.Errorf(".next/standalone/ not found. Ensure next.config.js has output: 'standalone'") } // Copy static assets into standalone staticSrc := filepath.Join(sourcePath, ".next", "static") staticDst := filepath.Join(standalonePath, ".next", "static") if _, err := os.Stat(staticSrc); err == nil { if err := copyDir(staticSrc, staticDst); err != nil { return fmt.Errorf("failed to copy static assets: %w", err) } } // Copy public directory if it exists publicSrc := filepath.Join(sourcePath, "public") publicDst := filepath.Join(standalonePath, "public") if _, err := os.Stat(publicSrc); err == nil { if err := copyDir(publicSrc, publicDst); err != nil { return fmt.Errorf("failed to copy public directory: %w", err) } } fmt.Printf("šŸ“¦ Creating tarball from standalone output...\n") tarball, err = createTarballAll(standalonePath) } else { // Static export: tarball the out/ directory outPath := filepath.Join(sourcePath, "out") if _, err := os.Stat(outPath); os.IsNotExist(err) { return fmt.Errorf("out/ directory not found. For static export, ensure next.config.js has output: 'export'") } fmt.Printf("šŸ“¦ Creating tarball from static export...\n") tarball, err = createTarball(outPath) } if err != nil { return fmt.Errorf("failed to create tarball: %w", err) } defer os.Remove(tarball) fmt.Printf("ā˜ļø Uploading to Orama Network...\n") endpoint := "/v1/deployments/nextjs/upload" if deployUpdate { endpoint = "/v1/deployments/nextjs/update" } resp, err := uploadDeployment(endpoint, tarball, map[string]string{ "name": deployName, "subdomain": deploySubdomain, "ssr": fmt.Sprintf("%t", deploySSR), }) if err != nil { return err } fmt.Printf("\nāœ… Deployment successful!\n\n") printDeploymentInfo(resp) if deploySSR { fmt.Printf("āš ļø Note: SSR deployment may take a minute to start. Check status with: orama deployments get %s\n", deployName) } return nil } func deployGo(cmd *cobra.Command, args []string) error { sourcePath, err := filepath.Abs(args[0]) if err != nil { return fmt.Errorf("failed to resolve path: %w", err) } // Verify it's a Go project if _, err := os.Stat(filepath.Join(sourcePath, "go.mod")); os.IsNotExist(err) { return fmt.Errorf("no go.mod found in %s", sourcePath) } // Cross-compile for Linux amd64 (production VPS target) fmt.Printf("šŸ”Ø Building Go binary (linux/amd64)...\n") buildCmd := exec.Command("go", "build", "-o", "app", ".") buildCmd.Dir = sourcePath buildCmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=amd64", "CGO_ENABLED=0") buildCmd.Stdout = os.Stdout buildCmd.Stderr = os.Stderr if err := buildCmd.Run(); err != nil { return fmt.Errorf("go build failed: %w", err) } defer os.Remove(filepath.Join(sourcePath, "app")) // Clean up after tarball fmt.Printf("šŸ“¦ Creating tarball...\n") tarball, err := createTarballFiles(sourcePath, []string{"app"}) if err != nil { return fmt.Errorf("failed to create tarball: %w", err) } defer os.Remove(tarball) fmt.Printf("ā˜ļø Uploading to Orama Network...\n") endpoint := "/v1/deployments/go/upload" if deployUpdate { endpoint = "/v1/deployments/go/update" } resp, err := uploadDeployment(endpoint, tarball, map[string]string{ "name": deployName, "subdomain": deploySubdomain, }) if err != nil { return err } fmt.Printf("\nāœ… Deployment successful!\n\n") printDeploymentInfo(resp) return nil } func deployNodeJS(cmd *cobra.Command, args []string) error { sourcePath, err := filepath.Abs(args[0]) if err != nil { return fmt.Errorf("failed to resolve path: %w", err) } // Verify it's a Node.js project if _, err := os.Stat(filepath.Join(sourcePath, "package.json")); os.IsNotExist(err) { return fmt.Errorf("no package.json found in %s", sourcePath) } // Install dependencies if needed if _, err := os.Stat(filepath.Join(sourcePath, "node_modules")); os.IsNotExist(err) { fmt.Printf("šŸ“¦ Installing dependencies...\n") if err := runBuildCommand(sourcePath, "npm", "install", "--production"); err != nil { return fmt.Errorf("npm install failed: %w", err) } } // Run build script if it exists if hasBuildScript(sourcePath) { fmt.Printf("šŸ”Ø Building...\n") if err := runBuildCommand(sourcePath, "npm", "run", "build"); err != nil { return fmt.Errorf("build failed: %w", err) } } fmt.Printf("šŸ“¦ Creating tarball...\n") tarball, err := createTarball(sourcePath) if err != nil { return fmt.Errorf("failed to create tarball: %w", err) } defer os.Remove(tarball) fmt.Printf("ā˜ļø Uploading to Orama Network...\n") endpoint := "/v1/deployments/nodejs/upload" if deployUpdate { endpoint = "/v1/deployments/nodejs/update" } resp, err := uploadDeployment(endpoint, tarball, map[string]string{ "name": deployName, "subdomain": deploySubdomain, }) if err != nil { return err } fmt.Printf("\nāœ… Deployment successful!\n\n") printDeploymentInfo(resp) return nil } // runBuildCommand runs a command in the given directory with stdout/stderr streaming func runBuildCommand(dir string, name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Dir = dir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } // hasBuildScript checks if package.json has a "build" script func hasBuildScript(dir string) bool { data, err := os.ReadFile(filepath.Join(dir, "package.json")) if err != nil { return false } var pkg map[string]interface{} if err := json.Unmarshal(data, &pkg); err != nil { return false } scripts, ok := pkg["scripts"].(map[string]interface{}) if !ok { return false } _, ok = scripts["build"] return ok } // copyDir recursively copies a directory func copyDir(src, dst string) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { if err != nil { return err } relPath, err := filepath.Rel(src, path) if err != nil { return err } dstPath := filepath.Join(dst, relPath) if info.IsDir() { return os.MkdirAll(dstPath, info.Mode()) } data, err := os.ReadFile(path) if err != nil { return err } return os.WriteFile(dstPath, data, info.Mode()) }) } // createTarballFiles creates a tarball containing only specific files from a directory func createTarballFiles(baseDir string, files []string) (string, error) { tmpFile, err := os.CreateTemp("", "orama-deploy-*.tar.gz") if err != nil { return "", err } defer tmpFile.Close() gzWriter := gzip.NewWriter(tmpFile) defer gzWriter.Close() tarWriter := tar.NewWriter(gzWriter) defer tarWriter.Close() for _, f := range files { fullPath := filepath.Join(baseDir, f) info, err := os.Stat(fullPath) if err != nil { return "", fmt.Errorf("file %s not found: %w", f, err) } header, err := tar.FileInfoHeader(info, "") if err != nil { return "", err } header.Name = f if err := tarWriter.WriteHeader(header); err != nil { return "", err } if !info.IsDir() { file, err := os.Open(fullPath) if err != nil { return "", err } _, err = io.Copy(tarWriter, file) file.Close() if err != nil { return "", err } } } return tmpFile.Name(), nil } func createTarball(sourcePath string) (string, error) { return createTarballWithOptions(sourcePath, true) } // createTarballAll creates a tarball including node_modules and hidden dirs (for standalone output) func createTarballAll(sourcePath string) (string, error) { return createTarballWithOptions(sourcePath, false) } func createTarballWithOptions(sourcePath string, skipNodeModules bool) (string, error) { // Create temp file tmpFile, err := os.CreateTemp("", "orama-deploy-*.tar.gz") if err != nil { return "", err } defer tmpFile.Close() // Create gzip writer gzWriter := gzip.NewWriter(tmpFile) defer gzWriter.Close() // Create tar writer tarWriter := tar.NewWriter(gzWriter) defer tarWriter.Close() // Walk directory and add files err = filepath.Walk(sourcePath, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // Skip hidden files and node_modules (unless disabled) if skipNodeModules { if strings.HasPrefix(info.Name(), ".") && info.Name() != "." { if info.IsDir() { return filepath.SkipDir } return nil } if info.Name() == "node_modules" { return filepath.SkipDir } } // Create tar header header, err := tar.FileInfoHeader(info, "") if err != nil { return err } // Update header name to be relative to source relPath, err := filepath.Rel(sourcePath, path) if err != nil { return err } header.Name = relPath // Write header if err := tarWriter.WriteHeader(header); err != nil { return err } // Write file content if not a directory if !info.IsDir() { file, err := os.Open(path) if err != nil { return err } defer file.Close() _, err = io.Copy(tarWriter, file) return err } return nil }) return tmpFile.Name(), err } func uploadDeployment(endpoint, tarballPath string, formData map[string]string) (map[string]interface{}, error) { // Open tarball file, err := os.Open(tarballPath) if err != nil { return nil, err } defer file.Close() // Create multipart request body := &bytes.Buffer{} writer := multipart.NewWriter(body) // Add form fields for key, value := range formData { writer.WriteField(key, value) } // Add file part, err := writer.CreateFormFile("tarball", filepath.Base(tarballPath)) if err != nil { return nil, err } _, err = io.Copy(part, file) if err != nil { return nil, err } writer.Close() // Get API URL from config apiURL := getAPIURL() url := apiURL + endpoint // Create request req, err := http.NewRequest("POST", url, body) if err != nil { return nil, err } req.Header.Set("Content-Type", writer.FormDataContentType()) // Add auth header token, err := getAuthToken() if err != nil { return nil, fmt.Errorf("authentication required: %w", err) } req.Header.Set("Authorization", "Bearer "+token) // Send request client := &http.Client{} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // Read response respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("deployment failed: %s", string(respBody)) } // Parse response var result map[string]interface{} err = json.Unmarshal(respBody, &result) if err != nil { return nil, err } return result, nil } func printDeploymentInfo(resp map[string]interface{}) { fmt.Printf("Name: %s\n", resp["name"]) fmt.Printf("Type: %s\n", resp["type"]) fmt.Printf("Status: %s\n", resp["status"]) fmt.Printf("Version: %v\n", resp["version"]) if contentCID, ok := resp["content_cid"]; ok && contentCID != "" { fmt.Printf("Content CID: %s\n", contentCID) } if urls, ok := resp["urls"].([]interface{}); ok && len(urls) > 0 { fmt.Printf("\nURLs:\n") for _, url := range urls { fmt.Printf(" • %s\n", url) } } } func getAPIURL() string { // Check environment variable first if url := os.Getenv("ORAMA_API_URL"); url != "" { return url } // Get from active environment config return auth.GetDefaultGatewayURL() } func getAuthToken() (string, error) { // Check environment variable first if token := os.Getenv("ORAMA_TOKEN"); token != "" { return token, nil } // Try to get from enhanced credentials store store, err := auth.LoadEnhancedCredentials() if err != nil { return "", fmt.Errorf("failed to load credentials: %w", err) } gatewayURL := auth.GetDefaultGatewayURL() creds := store.GetDefaultCredential(gatewayURL) if creds == nil { return "", fmt.Errorf("no credentials found for %s. Run 'orama auth login' to authenticate", gatewayURL) } if !creds.IsValid() { return "", fmt.Errorf("credentials expired for %s. Run 'orama auth login' to re-authenticate", gatewayURL) } return creds.APIKey, nil }