Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,166 @@ deepscanbot doctor
deepscanbot completion bash
```

## JSON Output Mode

DeepScanBot supports a consistent `--json` flag across all commands that return structured data. When enabled, all output is written as valid JSON to `stdout`, while progress messages and diagnostics are sent to `stderr`.

### Usage

```bash
# Scan with JSON output
deepscanbot scan https://example.com --json

# Version with JSON output
deepscanbot version --json

# Doctor with JSON output
deepscanbot doctor --json
```

### Sample JSON Output

#### Scan Command

```bash
$ deepscanbot scan https://example.com depth=0 --json
{
"status": "success",
"data": {
"start_url": "https://example.com",
"output_file": "crawler_results.json",
"started_at": "2024-01-15T10:30:00Z",
"finished_at": "2024-01-15T10:30:05Z",
"duration_ms": 5000,
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"skipped": 0,
"discovered": 0,
"max_depth": 0
},
"urls": [
{
"url": "https://example.com",
"depth": 0,
"status_code": 200,
"content_type": "text/html",
"result": "passed"
}
],
"skipped": null
},
"meta": {
"timestamp": "2024-01-15T10:30:05Z",
"command": "scan",
"duration_ms": 5000
}
}
```

#### Version Command

```bash
$ deepscanbot version --json
{
"status": "success",
"data": {
"version": "1.0.0",
"name": "DeepScanBot CLI"
},
"meta": {
"timestamp": "2024-01-15T10:30:00Z",
"command": "version",
"duration_ms": 0
}
}
```

#### Doctor Command

```bash
$ deepscanbot doctor --json
{
"status": "success",
"data": {
"installed": true,
"executable": true,
"configured": true,
"checks_passed": 3,
"message": "All checks passed!"
},
"meta": {
"timestamp": "2024-01-15T10:30:00Z",
"command": "doctor",
"duration_ms": 0
}
}
```

#### Error Response

```bash
$ deepscanbot scan invalid-url --json
{
"status": "error",
"error": {
"message": "invalid URL \"invalid-url\": must be an absolute http:// or https:// URL",
"code": "invalid_url"
},
"meta": {
"timestamp": "2024-01-15T10:30:00Z",
"command": "scan",
"duration_ms": 0
}
}
```

### Key Features

- **Valid JSON Only**: `stdout` contains only valid JSON when `--json` is enabled
- **Separate Streams**: Progress messages, warnings, and logs are written to `stderr`
- **Consistent Format**: All commands use the same JSON response structure
- **Backward Compatible**: Existing behavior is preserved when `--json` is not specified
- **Extensible**: The centralized output formatting makes it easy to add new output formats in the future

### Response Format

All JSON responses follow this structure:

```json
{
"status": "success | error",
"data": { ... }, // Present on success
"error": { // Present on error
"message": "Error description",
"code": "error_code"
},
"meta": {
"timestamp": "ISO8601 timestamp",
"command": "command_name",
"duration_ms": 1234
}
}
```

### Piping and Automation

The JSON output mode is designed for easy integration with scripts and tools:

```bash
# Parse with jq
deepscanbot scan https://example.com --json | jq '.data.summary'

# Extract specific fields
deepscanbot version --json | jq -r '.data.version'

# Check command success
if deepscanbot scan https://example.com --json | jq -e '.status == "success"'; then
echo "Scan completed successfully"
fi
```

## Usage

DeepScanBot uses a modern command-based CLI structure similar to git, docker, and kubectl.
Expand Down
124 changes: 113 additions & 11 deletions apps/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/mindfiredigital/DeepScanBot/packages/crawler"
"github.com/mindfiredigital/DeepScanBot/packages/logger"
"github.com/mindfiredigital/DeepScanBot/packages/output"
"github.com/mindfiredigital/DeepScanBot/packages/storage"
)

Expand Down Expand Up @@ -204,6 +205,12 @@ Examples:
log.Fatalf(err.Error())
}

// Check for --json flag (persistent flag from root command)
jsonFlag, _ := cmd.Flags().GetBool("json")
if jsonFlag {
opts.JSON = true
}

timeoutDuration := time.Duration(opts.Timeout) * time.Second

outputFilename, err := buildOutputFilename(opts.Output, opts.JSON)
Expand Down Expand Up @@ -234,6 +241,10 @@ Examples:
log.Fatalf("error: %v", err)
}

// Create output formatter
formatter := output.NewFormatter(opts.JSON)

// Write to file
if opts.JSON {
err = storage.WriteJSONReportToFile(outputFilename, report)
} else {
Expand All @@ -244,6 +255,15 @@ Examples:
log.Fatalf("write results: %v", err)
}

// If JSON mode, write report to stdout
if opts.JSON {
meta := output.NewResponseMetadata("scan", time.Duration(report.DurationMS)*time.Millisecond)
err = formatter.WriteSuccess(os.Stdout, report, meta)
if err != nil {
log.Fatalf("write JSON output: %v", err)
}
}

log.Infof("Results written to %s", outputFilename)
},
}
Expand All @@ -253,7 +273,33 @@ var versionCmd = &cobra.Command{
Short: "Show the installed version",
Long: `Display the current version of DeepScanBot CLI.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("DeepScanBot CLI %s\n", version)
// Check for --json flag or json=true option
jsonFlag, _ := cmd.Flags().GetBool("json")

// Also check if json=true was passed as a key=value option
jsonOption := false
for _, arg := range args {
if strings.HasPrefix(strings.ToLower(arg), "json=") {
parts := strings.SplitN(arg, "=", 2)
jsonOption = len(parts) == 2 && strings.ToLower(parts[1]) == "true"
break
}
}

if jsonFlag || jsonOption {
formatter := output.NewFormatter(true)
meta := output.NewResponseMetadata("version", 0)
data := map[string]string{
"version": version,
"name": "DeepScanBot CLI",
}
err := formatter.WriteSuccess(os.Stdout, data, meta)
if err != nil {
log.Fatalf("write JSON output: %v", err)
}
} else {
fmt.Printf("DeepScanBot CLI %s\n", version)
}
},
}

Expand All @@ -262,11 +308,40 @@ var doctorCmd = &cobra.Command{
Short: "Verify installation and environment",
Long: `Check that DeepScanBot is properly installed and the environment is configured correctly.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Running diagnostics...")
fmt.Println("✓ DeepScanBot is installed")
fmt.Println("✓ Binary is executable")
fmt.Println("✓ Environment is configured correctly")
fmt.Println("\nAll checks passed!")
// Check for --json flag or json=true option
jsonFlag, _ := cmd.Flags().GetBool("json")

// Also check if json=true was passed as a key=value option
jsonOption := false
for _, arg := range args {
if strings.HasPrefix(strings.ToLower(arg), "json=") {
parts := strings.SplitN(arg, "=", 2)
jsonOption = len(parts) == 2 && strings.ToLower(parts[1]) == "true"
break
}
}

if jsonFlag || jsonOption {
formatter := output.NewFormatter(true)
meta := output.NewResponseMetadata("doctor", 0)
data := map[string]interface{}{
"installed": true,
"executable": true,
"configured": true,
"checks_passed": 3,
"message": "All checks passed!",
}
err := formatter.WriteSuccess(os.Stdout, data, meta)
if err != nil {
log.Fatalf("write JSON output: %v", err)
}
} else {
fmt.Println("Running diagnostics...")
fmt.Println("✓ DeepScanBot is installed")
fmt.Println("✓ Binary is executable")
fmt.Println("✓ Environment is configured correctly")
fmt.Println("\nAll checks passed!")
}
},
}

Expand All @@ -283,15 +358,42 @@ var completionCmd = &cobra.Command{
Args: cobra.OnlyValidArgs,
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
Run: func(cmd *cobra.Command, args []string) {
shell := "bash"
if len(args) > 0 {
shell = args[0]
// Check for --json flag
jsonFlag, _ := cmd.Flags().GetBool("json")

if jsonFlag {
formatter := output.NewFormatter(true)
meta := output.NewResponseMetadata("completion", 0)
shell := "bash"
if len(args) > 0 {
shell = args[0]
}
data := map[string]interface{}{
"shell": shell,
"script": "Completion script generation not yet implemented",
"note": "Use the shell-specific completion commands instead",
"valid_shells": []string{"bash", "zsh", "fish", "powershell"},
}
err := formatter.WriteSuccess(os.Stdout, data, meta)
if err != nil {
log.Fatalf("write JSON output: %v", err)
}
} else {
shell := "bash"
if len(args) > 0 {
shell = args[0]
}
fmt.Printf("Generating %s completion script...\n", shell)
fmt.Println("Note: Shell completion script generation is not yet implemented.")
fmt.Println("Please use the standard Cobra completion commands.")
}
_ = shell
},
}

func init() {
// Add --json flag to root command so all subcommands can use it
rootCmd.PersistentFlags().Bool("json", false, "Output results in JSON format")

rootCmd.AddCommand(scanCmd, versionCmd, doctorCmd, configCmd, completionCmd)
}

Expand Down Expand Up @@ -341,4 +443,4 @@ func parseContentTypes(value string) []string {
}

return contentTypes
}
}
Loading