package pulsecli import ( "fmt" "os" "path/filepath" "strconv" "strings" "github.com/rcourtman/pulse-go-rewrite/internal/config" mockruntime "github.com/rcourtman/pulse-go-rewrite/internal/mock" "github.com/spf13/cobra" ) func GetMockEnvPath(mock *MockDeps) string { dataDir := strings.TrimSpace(os.Getenv("PULSE_DATA_DIR")) if dataDir != "" { return filepath.Join(dataDir, ".env") } if info, err := mockStat(mock, "tmp/dev-config"); err == nil && info.IsDir() { return filepath.Join("tmp/dev-config", ".env") } probe := filepath.Join(mockDefaultEnvDir(mock), ".env") if _, err := mockStat(mock, probe); err == nil { return probe } return filepath.Join(config.ResolveRuntimeDataDir(""), ".env") } func newMockCmd(mock *MockDeps) *cobra.Command { mockCmd := &cobra.Command{ Use: "mock", Short: "Manage mock/demo mode for development and demos", Long: `Enable or disable mock mode to run Pulse with simulated data instead of real infrastructure.`, } mockEnableCmd := &cobra.Command{ Use: "enable", Short: "Enable mock mode with simulated infrastructure data", Long: `Enable mock mode to run Pulse with simulated data. This creates/updates the runtime .env file and requires a service restart. Mock mode is useful for: - Demos without real infrastructure - Development and testing - Showcasing AI patrol features Example: pulse mock enable sudo systemctl restart pulse`, Run: func(cmd *cobra.Command, args []string) { if err := setMockMode(mock, true); err != nil { fmt.Fprintf(cmd.ErrOrStderr(), "Error: %v\n", err) mockExit(mock, 1) return } fmt.Fprintln(cmd.OutOrStdout(), "✓ Mock mode enabled") fmt.Fprintln(cmd.OutOrStdout()) fmt.Fprintln(cmd.OutOrStdout(), "Restart Pulse to apply changes:") fmt.Fprintln(cmd.OutOrStdout(), " sudo systemctl restart pulse") }, } mockDisableCmd := &cobra.Command{ Use: "disable", Short: "Disable mock mode and use real infrastructure", Long: `Disable mock mode to reconnect to real infrastructure. This updates the runtime .env file and requires a service restart. Example: pulse mock disable sudo systemctl restart pulse`, Run: func(cmd *cobra.Command, args []string) { if err := setMockMode(mock, false); err != nil { fmt.Fprintf(cmd.ErrOrStderr(), "Error: %v\n", err) mockExit(mock, 1) return } fmt.Fprintln(cmd.OutOrStdout(), "✓ Mock mode disabled") fmt.Fprintln(cmd.OutOrStdout()) fmt.Fprintln(cmd.OutOrStdout(), "Restart Pulse to apply changes:") fmt.Fprintln(cmd.OutOrStdout(), " sudo systemctl restart pulse") }, } mockStatusCmd := &cobra.Command{ Use: "status", Short: "Show current mock mode status", Run: func(cmd *cobra.Command, args []string) { enabled, config := getMockStatus(mock) if enabled { fmt.Fprintln(cmd.OutOrStdout(), "Mock mode: ENABLED") fmt.Fprintln(cmd.OutOrStdout()) fmt.Fprintln(cmd.OutOrStdout(), "Configuration:") for _, line := range config { fmt.Fprintf(cmd.OutOrStdout(), " %s\n", line) } return } fmt.Fprintln(cmd.OutOrStdout(), "Mock mode: DISABLED") fmt.Fprintln(cmd.OutOrStdout()) fmt.Fprintln(cmd.OutOrStdout(), "Run 'pulse mock enable' to enable mock mode") }, } mockCmd.AddCommand(mockEnableCmd) mockCmd.AddCommand(mockDisableCmd) mockCmd.AddCommand(mockStatusCmd) return mockCmd } func setMockMode(mock *MockDeps, enable bool) error { envPath := GetMockEnvPath(mock) config := getDefaultMockConfig() if data, err := os.ReadFile(envPath); err == nil { existing := parseMockEnv(string(data)) for k, v := range existing { if k != "PULSE_MOCK_MODE" { config[k] = v } } } if enable { config["PULSE_MOCK_MODE"] = "true" } else { config["PULSE_MOCK_MODE"] = "false" } return writeMockEnv(envPath, config) } func getMockStatus(mock *MockDeps) (bool, []string) { envPath := GetMockEnvPath(mock) data, err := os.ReadFile(envPath) if err != nil { return false, nil } config := parseMockEnv(string(data)) enabled := strings.EqualFold(strings.TrimSpace(config["PULSE_MOCK_MODE"]), "true") lines := make([]string, 0, len(config)) for key, value := range config { lines = append(lines, fmt.Sprintf("%s=%s", key, value)) } return enabled, lines } func getDefaultMockConfig() map[string]string { cfg := mockruntime.DefaultConfig return map[string]string{ "PULSE_MOCK_MODE": "false", "PULSE_MOCK_NODES": strconv.Itoa(cfg.NodeCount), "PULSE_MOCK_VMS_PER_NODE": strconv.Itoa(cfg.VMsPerNode), "PULSE_MOCK_LXCS_PER_NODE": strconv.Itoa(cfg.LXCsPerNode), "PULSE_MOCK_DOCKER_HOSTS": strconv.Itoa(cfg.DockerHostCount), "PULSE_MOCK_DOCKER_CONTAINERS": strconv.Itoa(cfg.DockerContainersPerHost), "PULSE_MOCK_GENERIC_HOSTS": strconv.Itoa(cfg.GenericHostCount), "PULSE_MOCK_K8S_CLUSTERS": strconv.Itoa(cfg.K8sClusterCount), "PULSE_MOCK_K8S_NODES": strconv.Itoa(cfg.K8sNodesPerCluster), "PULSE_MOCK_K8S_PODS": strconv.Itoa(cfg.K8sPodsPerCluster), "PULSE_MOCK_K8S_DEPLOYMENTS": strconv.Itoa(cfg.K8sDeploymentsPerCluster), "PULSE_MOCK_RANDOM_METRICS": strconv.FormatBool(cfg.RandomMetrics), "PULSE_MOCK_STOPPED_PERCENT": strconv.FormatFloat(cfg.StoppedPercent*100, 'f', -1, 64), } } func parseMockEnv(content string) map[string]string { result := make(map[string]string) for _, line := range strings.Split(content, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } parts := strings.SplitN(line, "=", 2) if len(parts) == 2 { result[parts[0]] = parts[1] } } return result } func writeMockEnv(envPath string, config map[string]string) error { var b strings.Builder b.WriteString("# Pulse Mock Mode Configuration\n") b.WriteString("# Enable with: pulse mock enable\n") b.WriteString("# Disable with: pulse mock disable\n\n") for _, key := range []string{ "PULSE_MOCK_MODE", "PULSE_MOCK_NODES", "PULSE_MOCK_VMS_PER_NODE", "PULSE_MOCK_LXCS_PER_NODE", "PULSE_MOCK_DOCKER_HOSTS", "PULSE_MOCK_DOCKER_CONTAINERS", "PULSE_MOCK_GENERIC_HOSTS", "PULSE_MOCK_K8S_CLUSTERS", "PULSE_MOCK_K8S_NODES", "PULSE_MOCK_K8S_PODS", "PULSE_MOCK_K8S_DEPLOYMENTS", "PULSE_MOCK_RANDOM_METRICS", "PULSE_MOCK_STOPPED_PERCENT", } { if value, ok := config[key]; ok { fmt.Fprintf(&b, "%s=%s\n", key, value) } } for key, value := range config { if strings.HasPrefix(key, "PULSE_MOCK_") { continue } fmt.Fprintf(&b, "%s=%s\n", key, value) } return os.WriteFile(envPath, []byte(b.String()), 0644) }