diff --git a/.gitignore b/.gitignore index 022143d..bcf910e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ dist/ .env +.claude/ diff --git a/cmd/openstatus/main.go b/cmd/openstatus/main.go index d705c4d..bfc21df 100644 --- a/cmd/openstatus/main.go +++ b/cmd/openstatus/main.go @@ -1,8 +1,8 @@ package main import ( - cmd "github.com/openstatusHQ/cli/internal/cmd" "github.com/joho/godotenv" + cmd "github.com/openstatusHQ/cli/internal/cmd" "log" ) diff --git a/docs/openstatus-docs.md b/docs/openstatus-docs.md index c72c80d..08cdaa5 100644 --- a/docs/openstatus-docs.md +++ b/docs/openstatus-docs.md @@ -14,12 +14,12 @@ $ openstatus [GLOBAL FLAGS] [COMMAND] [COMMAND FLAGS] [ARGUMENTS...] Global flags: -| Name | Description | Default value | Environment variables | -|------------------|---------------------------|:-------------:|:---------------------:| -| `--json` | Output results as JSON | `false` | *none* | -| `--no-color` | Disable colored output | `false` | *none* | -| `--quiet` (`-q`) | Suppress non-error output | `false` | *none* | -| `--debug` | Enable debug output | `false` | *none* | +| Name | Description | Type | Default value | Environment variables | +|------------------|---------------------------|------|:-------------:|:---------------------:| +| `--json` | Output results as JSON | bool | `false` | *none* | +| `--no-color` | Disable colored output | bool | `false` | *none* | +| `--quiet` (`-q`) | Suppress non-error output | bool | `false` | *none* | +| `--debug` | Enable debug output | bool | `false` | *none* | ### `monitors` command (aliases: `m`) @@ -49,12 +49,12 @@ $ openstatus [GLOBAL FLAGS] monitors apply [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-------------------------------------------------------|:-----------------:|:----------------------:| -| `--config="…"` (`-c`) | The configuration file containing monitor information | `openstatus.yaml` | *none* | -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--auto-accept` (`-y`) | Automatically accept the prompt | `false` | *none* | -| `--dry-run` (`-n`) | Show what would be changed without applying | `false` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-------------------------------------------------------|--------|:-------------------:|:----------------------:| +| `--config="…"` (`-c`) | The configuration file containing monitor information | string | `"openstatus.yaml"` | *none* | +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--auto-accept` (`-y`) | Automatically accept the prompt | bool | `false` | *none* | +| `--dry-run` (`-n`) | Show what would be changed without applying | bool | `false` | *none* | ### `monitors create` subcommand @@ -73,11 +73,11 @@ $ openstatus [GLOBAL FLAGS] monitors create [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-------------------------------------------------------|:-----------------:|:----------------------:| -| `--config="…"` (`-c`) | The configuration file containing monitor information | `openstatus.yaml` | *none* | -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--auto-accept` (`-y`) | Automatically accept the prompt | `false` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-------------------------------------------------------|--------|:-------------------:|:----------------------:| +| `--config="…"` (`-c`) | The configuration file containing monitor information | string | `"openstatus.yaml"` | *none* | +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--auto-accept` (`-y`) | Automatically accept the prompt | bool | `false` | *none* | ### `monitors delete` subcommand @@ -94,10 +94,10 @@ $ openstatus [GLOBAL FLAGS] monitors delete [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|---------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--auto-accept` (`-y`) | Automatically accept the prompt | `false` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|---------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--auto-accept` (`-y`) | Automatically accept the prompt | bool | `false` | *none* | ### `monitors import` subcommand @@ -116,10 +116,10 @@ $ openstatus [GLOBAL FLAGS] monitors import [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-----------------------------|:-----------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--output="…"` (`-o`) | The output file name | `openstatus.yaml` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-----------------------------|--------|:-------------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--output="…"` (`-o`) | The output file name | string | `"openstatus.yaml"` | *none* | ### `monitors info` subcommand @@ -139,10 +139,10 @@ $ openstatus [GLOBAL FLAGS] monitors info [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|----------------------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--time-range="…"` | Time range for summary metrics (1d, 7d, 14d) | `1d` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|----------------------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--time-range="…"` | Time range for summary metrics (1d, 7d, 14d) | string | `"1d"` | *none* | ### `monitors list` subcommand @@ -161,10 +161,10 @@ $ openstatus [GLOBAL FLAGS] monitors list [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-------------------------------------------|:-------------:|:----------------------:| -| `--all` | List all monitors including inactive ones | `false` | *none* | -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-------------------------------------------|--------|:-------------:|:----------------------:| +| `--all` | List all monitors including inactive ones | bool | `false` | *none* | +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | ### `monitors logs` subcommand @@ -186,13 +186,13 @@ $ openstatus [GLOBAL FLAGS] monitors logs [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|------------------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--limit="…"` | Maximum number of logs to return (1-100) | `0` | *none* | -| `--offset="…"` | Number of logs to skip for pagination | `0` | *none* | -| `--from="…"` | Start of time window (RFC 3339 format) | | *none* | -| `--to="…"` | End of time window (RFC 3339 format) | | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|------------------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--limit="…"` | Maximum number of logs to return (1-100) | int | `0` | *none* | +| `--offset="…"` | Number of logs to skip for pagination | int | `0` | *none* | +| `--from="…"` | Start of time window (RFC 3339 format) | string | | *none* | +| `--to="…"` | End of time window (RFC 3339 format) | string | | *none* | ### `monitors log-info` subcommand @@ -211,9 +211,9 @@ $ openstatus [GLOBAL FLAGS] monitors log-info [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-----------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-----------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | ### `monitors trigger` subcommand @@ -232,9 +232,9 @@ $ openstatus [GLOBAL FLAGS] monitors trigger [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-----------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-----------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | ### `status-report` command (aliases: `sr`) @@ -261,11 +261,11 @@ $ openstatus [GLOBAL FLAGS] status-report list [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|--------------------------------------------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--status="…"` | Filter by status (investigating, identified, monitoring, resolved) | | *none* | -| `--limit="…"` | Maximum number of reports to return (1-100) | `0` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|--------------------------------------------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--status="…"` | Filter by status (investigating, identified, monitoring, resolved) | string | | *none* | +| `--limit="…"` | Maximum number of reports to return (1-100) | int | `0` | *none* | ### `status-report info` subcommand @@ -282,9 +282,9 @@ $ openstatus [GLOBAL FLAGS] status-report info [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-----------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-----------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | ### `status-report create` subcommand @@ -300,16 +300,16 @@ $ openstatus [GLOBAL FLAGS] status-report create [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|------------------------------------------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--title="…"` | Title of the status report | | *none* | -| `--status="…"` | Initial status (investigating, identified, monitoring, resolved) | | *none* | -| `--message="…"` | Initial message describing the incident | | *none* | -| `--page-id="…"` | Status page ID to associate with this report | | *none* | -| `--component-ids="…"` | Comma-separated page component IDs | | *none* | -| `--notify` | Notify subscribers about this status report | `false` | *none* | -| `--date="…"` | Date when the event occurred (RFC 3339 format, defaults to now) | | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|------------------------------------------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--title="…"` | Title of the status report | string | | *none* | +| `--status="…"` | Initial status (investigating, identified, monitoring, resolved) | string | | *none* | +| `--message="…"` | Initial message describing the incident | string | | *none* | +| `--page-id="…"` | Status page ID to associate with this report | string | | *none* | +| `--component-ids="…"` | Comma-separated page component IDs | string | | *none* | +| `--notify` | Notify subscribers about this status report | bool | `false` | *none* | +| `--date="…"` | Date when the event occurred (RFC 3339 format, defaults to now) | string | | *none* | ### `status-report update` subcommand @@ -325,11 +325,11 @@ $ openstatus [GLOBAL FLAGS] status-report update [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-------------------------------------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--title="…"` | New title for the report | | *none* | -| `--component-ids="…"` | Comma-separated page component IDs (replaces existing list) | | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-------------------------------------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--title="…"` | New title for the report | string | | *none* | +| `--component-ids="…"` | Comma-separated page component IDs (replaces existing list) | string | | *none* | ### `status-report delete` subcommand @@ -346,10 +346,10 @@ $ openstatus [GLOBAL FLAGS] status-report delete [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|---------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--auto-accept` (`-y`) | Automatically accept the prompt | `false` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|---------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--auto-accept` (`-y`) | Automatically accept the prompt | bool | `false` | *none* | ### `status-report add-update` subcommand @@ -365,13 +365,13 @@ $ openstatus [GLOBAL FLAGS] status-report add-update [COMMAND FLAGS] [ARGUMENTS. The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|--------------------------------------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--status="…"` | New status (investigating, identified, monitoring, resolved) | | *none* | -| `--message="…"` | Message describing what changed | | *none* | -| `--date="…"` | Date for the update (RFC 3339 format, defaults to now) | | *none* | -| `--notify` | Notify subscribers about this update | `false` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|--------------------------------------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--status="…"` | New status (investigating, identified, monitoring, resolved) | string | | *none* | +| `--message="…"` | Message describing what changed | string | | *none* | +| `--date="…"` | Date for the update (RFC 3339 format, defaults to now) | string | | *none* | +| `--notify` | Notify subscribers about this update | bool | `false` | *none* | ### `maintenance` command (aliases: `mt`) @@ -398,11 +398,11 @@ $ openstatus [GLOBAL FLAGS] maintenance list [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|--------------------------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--page-id="…"` | Filter by status page ID | | *none* | -| `--limit="…"` | Maximum number of maintenances to return (1-100) | `0` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|--------------------------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--page-id="…"` | Filter by status page ID | string | | *none* | +| `--limit="…"` | Maximum number of maintenances to return (1-100) | int | `0` | *none* | ### `maintenance info` subcommand @@ -419,9 +419,9 @@ $ openstatus [GLOBAL FLAGS] maintenance info [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-----------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-----------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | ### `maintenance create` subcommand @@ -437,16 +437,16 @@ $ openstatus [GLOBAL FLAGS] maintenance create [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|--------------------------------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--title="…"` | Title of the maintenance | | *none* | -| `--message="…"` | Message describing the maintenance | | *none* | -| `--from="…"` | Start time of the maintenance window (RFC 3339 format) | | *none* | -| `--to="…"` | End time of the maintenance window (RFC 3339 format) | | *none* | -| `--page-id="…"` | Status page ID to associate with this maintenance | | *none* | -| `--component-ids="…"` | Comma-separated page component IDs | | *none* | -| `--notify` | Notify subscribers about this maintenance | `false` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|--------------------------------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--title="…"` | Title of the maintenance | string | | *none* | +| `--message="…"` | Message describing the maintenance | string | | *none* | +| `--from="…"` | Start time of the maintenance window (RFC 3339 format) | string | | *none* | +| `--to="…"` | End time of the maintenance window (RFC 3339 format) | string | | *none* | +| `--page-id="…"` | Status page ID to associate with this maintenance | string | | *none* | +| `--component-ids="…"` | Comma-separated page component IDs | string | | *none* | +| `--notify` | Notify subscribers about this maintenance | bool | `false` | *none* | ### `maintenance update` subcommand @@ -462,14 +462,14 @@ $ openstatus [GLOBAL FLAGS] maintenance update [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-------------------------------------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--title="…"` | New title for the maintenance | | *none* | -| `--message="…"` | New message for the maintenance | | *none* | -| `--from="…"` | New start time (RFC 3339 format) | | *none* | -| `--to="…"` | New end time (RFC 3339 format) | | *none* | -| `--component-ids="…"` | Comma-separated page component IDs (replaces existing list) | | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-------------------------------------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--title="…"` | New title for the maintenance | string | | *none* | +| `--message="…"` | New message for the maintenance | string | | *none* | +| `--from="…"` | New start time (RFC 3339 format) | string | | *none* | +| `--to="…"` | New end time (RFC 3339 format) | string | | *none* | +| `--component-ids="…"` | Comma-separated page component IDs (replaces existing list) | string | | *none* | ### `maintenance delete` subcommand @@ -486,10 +486,10 @@ $ openstatus [GLOBAL FLAGS] maintenance delete [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|---------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--auto-accept` (`-y`) | Automatically accept the prompt | `false` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|---------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--auto-accept` (`-y`) | Automatically accept the prompt | bool | `false` | *none* | ### `status-page` command (aliases: `sp`) @@ -516,10 +516,10 @@ $ openstatus [GLOBAL FLAGS] status-page list [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-------------------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--limit="…"` | Maximum number of pages to return (1-100) | `0` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-------------------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--limit="…"` | Maximum number of pages to return (1-100) | int | `0` | *none* | ### `status-page info` subcommand @@ -536,9 +536,9 @@ $ openstatus [GLOBAL FLAGS] status-page info [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-----------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-----------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | ### `notification` command (aliases: `n`) @@ -565,10 +565,10 @@ $ openstatus [GLOBAL FLAGS] notification list [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|---------------------------------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--limit="…"` | Maximum number of notifications to return (1-100) | `0` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|---------------------------------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--limit="…"` | Maximum number of notifications to return (1-100) | int | `0` | *none* | ### `notification info` subcommand @@ -585,9 +585,9 @@ $ openstatus [GLOBAL FLAGS] notification info [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-----------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-----------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | ### `run` command (aliases: `r`) @@ -606,10 +606,10 @@ $ openstatus [GLOBAL FLAGS] run [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-----------------------------|:------------------------:|:----------------------:| -| `--config="…"` | The configuration file | `config.openstatus.yaml` | *none* | -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-----------------------------|--------|:--------------------------:|:----------------------:| +| `--config="…"` | The configuration file | string | `"config.openstatus.yaml"` | *none* | +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | ### `whoami` command (aliases: `w`) @@ -627,9 +627,9 @@ $ openstatus [GLOBAL FLAGS] whoami [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-----------------------------|:-------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|-----------------------------|--------|:-------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | ### `login` command @@ -681,7 +681,8 @@ $ openstatus [GLOBAL FLAGS] terraform generate [COMMAND FLAGS] [ARGUMENTS...] The following flags are supported: -| Name | Description | Default value | Environment variables | -|-----------------------------|-----------------------------------------|:-------------------------:|:----------------------:| -| `--access-token="…"` (`-t`) | OpenStatus API Access Token | | `OPENSTATUS_API_TOKEN` | -| `--output-dir="…"` (`-o`) | Directory to write Terraform files into | `./openstatus-terraform/` | *none* | +| Name | Description | Type | Default value | Environment variables | +|-----------------------------|------------------------------------------|--------|:---------------------------:|:----------------------:| +| `--access-token="…"` (`-t`) | OpenStatus API Access Token | string | | `OPENSTATUS_API_TOKEN` | +| `--output-dir="…"` (`-o`) | Directory to write Terraform files into | string | `"./openstatus-terraform/"` | *none* | +| `--force` (`-f`) | Overwrite existing files in --output-dir | bool | `false` | *none* | diff --git a/docs/openstatus.1 b/docs/openstatus.1 index b2ab37e..10e4948 100644 --- a/docs/openstatus.1 +++ b/docs/openstatus.1 @@ -29,12 +29,14 @@ Global flags: .PP .TS tab(@); -lw(15.2n) lw(22.8n) cw(12.7n) cw(19.4n). +lw(14.2n) lw(21.2n) lw(4.7n) cw(11.8n) cw(18.1n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -45,6 +47,8 @@ T{ T}@T{ Output results as JSON T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -54,6 +58,8 @@ T{ T}@T{ Disable colored output T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -63,6 +69,8 @@ T{ T}@T{ Suppress non\-error output T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -72,6 +80,8 @@ T{ T}@T{ Enable debug output T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -108,12 +118,14 @@ The following flags are supported: .PP .TS tab(@); -lw(16.0n) lw(30.3n) cw(10.5n) cw(13.2n). +lw(14.8n) lw(28.1n) lw(4.1n) cw(10.7n) cw(12.3n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -124,7 +136,9 @@ T{ T}@T{ The configuration file containing monitor information T}@T{ -\f[CR]openstatus.yaml\f[R] +string +T}@T{ +\f[CR]\(dqopenstatus.yaml\(dq\f[R] T}@T{ \f[I]none\f[R] T} @@ -133,6 +147,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -141,6 +157,8 @@ T{ T}@T{ Automatically accept the prompt T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -150,6 +168,8 @@ T{ T}@T{ Show what would be changed without applying T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -175,12 +195,14 @@ The following flags are supported: .PP .TS tab(@); -lw(16.0n) lw(30.3n) cw(10.5n) cw(13.2n). +lw(14.8n) lw(28.1n) lw(4.1n) cw(10.7n) cw(12.3n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -191,7 +213,9 @@ T{ T}@T{ The configuration file containing monitor information T}@T{ -\f[CR]openstatus.yaml\f[R] +string +T}@T{ +\f[CR]\(dqopenstatus.yaml\(dq\f[R] T}@T{ \f[I]none\f[R] T} @@ -200,6 +224,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -208,6 +234,8 @@ T{ T}@T{ Automatically accept the prompt T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -230,12 +258,14 @@ The following flags are supported: .PP .TS tab(@); -lw(20.1n) lw(22.9n) cw(10.4n) cw(16.6n). +lw(18.6n) lw(21.2n) lw(5.1n) cw(9.6n) cw(15.4n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -246,6 +276,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -254,6 +286,8 @@ T{ T}@T{ Automatically accept the prompt T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -280,12 +314,14 @@ The following flags are supported: .PP .TS tab(@); -lw(20.1n) lw(20.1n) cw(13.2n) cw(16.6n). +lw(18.3n) lw(18.3n) lw(5.0n) cw(13.2n) cw(15.1n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -296,6 +332,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -304,7 +342,9 @@ T{ T}@T{ The output file name T}@T{ -\f[CR]openstatus.yaml\f[R] +string +T}@T{ +\f[CR]\(dqopenstatus.yaml\(dq\f[R] T}@T{ \f[I]none\f[R] T} @@ -330,12 +370,14 @@ The following flags are supported: .PP .TS tab(@); -lw(17.8n) lw(28.2n) cw(9.2n) cw(14.7n). +lw(16.6n) lw(26.4n) lw(4.6n) cw(8.6n) cw(13.8n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -346,6 +388,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -354,7 +398,9 @@ T{ T}@T{ Time range for summary metrics (1d, 7d, 14d) T}@T{ -\f[CR]1d\f[R] +string +T}@T{ +\f[CR]\(dq1d\(dq\f[R] T}@T{ \f[I]none\f[R] T} @@ -380,12 +426,14 @@ The following flags are supported: .PP .TS tab(@); -lw(18.3n) lw(27.1n) cw(9.5n) cw(15.1n). +lw(17.1n) lw(25.3n) lw(4.7n) cw(8.8n) cw(14.1n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -396,6 +444,8 @@ T{ T}@T{ List all monitors including inactive ones T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -405,6 +455,142 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ +T}@T{ +\f[CR]OPENSTATUS_API_TOKEN\f[R] +T} +.TE +.SS \f[CR]monitors logs\f[R] subcommand +List HTTP response logs for a monitor. +.RS +.PP +openstatus monitors logs openstatus monitors logs 12345 openstatus +monitors logs 12345 \(enlimit 10 openstatus monitors logs 12345 +\(enlimit 5 \(enoffset 5 openstatus monitors logs 12345 \(enfrom +2026\-05\-06T00:00:00Z \(ento 2026\-05\-07T00:00:00Z +.RE +.PP +List HTTP response logs for a monitor from the 14\-day retention window. +Supports pagination and time filtering. +.PP +Usage: +.IP +.EX +$ openstatus [GLOBAL FLAGS] monitors logs [COMMAND FLAGS] [ARGUMENTS...] +.EE +.PP +The following flags are supported: +.PP +.TS +tab(@); +lw(17.2n) lw(24.9n) lw(4.7n) cw(8.9n) cw(14.2n). +T{ +Name +T}@T{ +Description +T}@T{ +Type +T}@T{ +Default value +T}@T{ +Environment variables +T} +_ +T{ +\f[CR]\-\-access\-token=\(dq\&...\(dq\f[R] (\f[CR]\-t\f[R]) +T}@T{ +OpenStatus API Access Token +T}@T{ +string +T}@T{ +T}@T{ +\f[CR]OPENSTATUS_API_TOKEN\f[R] +T} +T{ +\f[CR]\-\-limit=\(dq\&...\(dq\f[R] +T}@T{ +Maximum number of logs to return (1\-100) +T}@T{ +int +T}@T{ +\f[CR]0\f[R] +T}@T{ +\f[I]none\f[R] +T} +T{ +\f[CR]\-\-offset=\(dq\&...\(dq\f[R] +T}@T{ +Number of logs to skip for pagination +T}@T{ +int +T}@T{ +\f[CR]0\f[R] +T}@T{ +\f[I]none\f[R] +T} +T{ +\f[CR]\-\-from=\(dq\&...\(dq\f[R] +T}@T{ +Start of time window (RFC 3339 format) +T}@T{ +string +T}@T{ +T}@T{ +\f[I]none\f[R] +T} +T{ +\f[CR]\-\-to=\(dq\&...\(dq\f[R] +T}@T{ +End of time window (RFC 3339 format) +T}@T{ +string +T}@T{ +T}@T{ +\f[I]none\f[R] +T} +.TE +.SS \f[CR]monitors log\-info\f[R] subcommand +Get detailed HTTP response log for a monitor. +.RS +.PP +openstatus monitors log\-info openstatus monitors log\-info 12345 +abc\-def\-ghi +.RE +.PP +Fetch a single HTTP response log with full details including timing +phases, response headers, and assertion results. +.PP +Usage: +.IP +.EX +$ openstatus [GLOBAL FLAGS] monitors log\-info [COMMAND FLAGS] [ARGUMENTS...] +.EE +.PP +The following flags are supported: +.PP +.TS +tab(@); +lw(19.3n) lw(19.3n) lw(5.3n) cw(10.0n) cw(16.0n). +T{ +Name +T}@T{ +Description +T}@T{ +Type +T}@T{ +Default value +T}@T{ +Environment variables +T} +_ +T{ +\f[CR]\-\-access\-token=\(dq\&...\(dq\f[R] (\f[CR]\-t\f[R]) +T}@T{ +OpenStatus API Access Token +T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -429,12 +615,14 @@ The following flags are supported: .PP .TS tab(@); -lw(20.9n) lw(20.9n) cw(10.8n) cw(17.3n). +lw(19.3n) lw(19.3n) lw(5.3n) cw(10.0n) cw(16.0n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -445,6 +633,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -475,12 +665,14 @@ The following flags are supported: .PP .TS tab(@); -lw(14.9n) lw(35.0n) cw(7.7n) cw(12.4n). +lw(14.1n) lw(33.1n) lw(3.9n) cw(7.3n) cw(11.7n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -491,6 +683,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -499,6 +693,8 @@ T{ T}@T{ Filter by status (investigating, identified, monitoring, resolved) T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -507,6 +703,8 @@ T{ T}@T{ Maximum number of reports to return (1\-100) T}@T{ +int +T}@T{ \f[CR]0\f[R] T}@T{ \f[I]none\f[R] @@ -529,12 +727,14 @@ The following flags are supported: .PP .TS tab(@); -lw(20.9n) lw(20.9n) cw(10.8n) cw(17.3n). +lw(19.3n) lw(19.3n) lw(5.3n) cw(10.0n) cw(16.0n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -545,6 +745,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -568,12 +770,14 @@ The following flags are supported: .PP .TS tab(@); -lw(15.1n) lw(34.5n) cw(7.8n) cw(12.5n). +lw(14.3n) lw(32.5n) lw(3.9n) cw(7.4n) cw(11.8n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -584,6 +788,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -592,6 +798,8 @@ T{ T}@T{ Title of the status report T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -600,6 +808,8 @@ T{ T}@T{ Initial status (investigating, identified, monitoring, resolved) T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -608,6 +818,8 @@ T{ T}@T{ Initial message describing the incident T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -616,6 +828,8 @@ T{ T}@T{ Status page ID to associate with this report T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -624,6 +838,8 @@ T{ T}@T{ Comma\-separated page component IDs T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -632,6 +848,8 @@ T{ T}@T{ Notify subscribers about this status report T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -641,6 +859,8 @@ T{ T}@T{ Date when the event occurred (RFC 3339 format, defaults to now) T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -663,12 +883,14 @@ The following flags are supported: .PP .TS tab(@); -lw(15.7n) lw(33.1n) cw(8.1n) cw(13.0n). +lw(14.8n) lw(31.2n) lw(4.1n) cw(7.7n) cw(12.3n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -679,6 +901,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -687,6 +911,8 @@ T{ T}@T{ New title for the report T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -695,6 +921,8 @@ T{ T}@T{ Comma\-separated page component IDs (replaces existing list) T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -717,12 +945,14 @@ The following flags are supported: .PP .TS tab(@); -lw(20.1n) lw(22.9n) cw(10.4n) cw(16.6n). +lw(18.6n) lw(21.2n) lw(5.1n) cw(9.6n) cw(15.4n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -733,6 +963,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -741,6 +973,8 @@ T{ T}@T{ Automatically accept the prompt T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -764,12 +998,14 @@ The following flags are supported: .PP .TS tab(@); -lw(15.6n) lw(33.4n) cw(8.1n) cw(12.9n). +lw(14.7n) lw(31.4n) lw(4.1n) cw(7.6n) cw(12.2n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -780,6 +1016,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -788,6 +1026,8 @@ T{ T}@T{ New status (investigating, identified, monitoring, resolved) T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -796,6 +1036,8 @@ T{ T}@T{ Message describing what changed T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -804,6 +1046,8 @@ T{ T}@T{ Date for the update (RFC 3339 format, defaults to now) T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -812,6 +1056,8 @@ T{ T}@T{ Notify subscribers about this update T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -843,12 +1089,14 @@ The following flags are supported: .PP .TS tab(@); -lw(17.2n) lw(29.7n) cw(8.9n) cw(14.2n). +lw(16.1n) lw(27.8n) lw(4.4n) cw(8.3n) cw(13.3n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -859,6 +1107,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -867,6 +1117,8 @@ T{ T}@T{ Filter by status page ID T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -875,6 +1127,8 @@ T{ T}@T{ Maximum number of maintenances to return (1\-100) T}@T{ +int +T}@T{ \f[CR]0\f[R] T}@T{ \f[I]none\f[R] @@ -897,12 +1151,14 @@ The following flags are supported: .PP .TS tab(@); -lw(20.9n) lw(20.9n) cw(10.8n) cw(17.3n). +lw(19.3n) lw(19.3n) lw(5.3n) cw(10.0n) cw(16.0n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -913,6 +1169,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -936,12 +1194,14 @@ The following flags are supported: .PP .TS tab(@); -lw(16.4n) lw(31.6n) cw(8.5n) cw(13.5n). +lw(15.4n) lw(29.7n) lw(4.2n) cw(8.0n) cw(12.7n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -952,6 +1212,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -960,6 +1222,8 @@ T{ T}@T{ Title of the maintenance T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -968,6 +1232,8 @@ T{ T}@T{ Message describing the maintenance T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -976,6 +1242,8 @@ T{ T}@T{ Start time of the maintenance window (RFC 3339 format) T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -984,6 +1252,8 @@ T{ T}@T{ End time of the maintenance window (RFC 3339 format) T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -992,6 +1262,8 @@ T{ T}@T{ Status page ID to associate with this maintenance T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -1000,6 +1272,8 @@ T{ T}@T{ Comma\-separated page component IDs T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -1008,6 +1282,8 @@ T{ T}@T{ Notify subscribers about this maintenance T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -1032,12 +1308,14 @@ The following flags are supported: .PP .TS tab(@); -lw(15.7n) lw(33.1n) cw(8.1n) cw(13.0n). +lw(14.8n) lw(31.2n) lw(4.1n) cw(7.7n) cw(12.3n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -1048,6 +1326,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -1056,6 +1336,8 @@ T{ T}@T{ New title for the maintenance T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -1064,6 +1346,8 @@ T{ T}@T{ New message for the maintenance T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -1072,6 +1356,8 @@ T{ T}@T{ New start time (RFC 3339 format) T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -1080,6 +1366,8 @@ T{ T}@T{ New end time (RFC 3339 format) T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -1088,6 +1376,8 @@ T{ T}@T{ Comma\-separated page component IDs (replaces existing list) T}@T{ +string +T}@T{ T}@T{ \f[I]none\f[R] T} @@ -1109,12 +1399,14 @@ The following flags are supported: .PP .TS tab(@); -lw(20.1n) lw(22.9n) cw(10.4n) cw(16.6n). +lw(18.6n) lw(21.2n) lw(5.1n) cw(9.6n) cw(15.4n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -1125,6 +1417,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -1133,6 +1427,8 @@ T{ T}@T{ Automatically accept the prompt T}@T{ +bool +T}@T{ \f[CR]false\f[R] T}@T{ \f[I]none\f[R] @@ -1163,12 +1459,14 @@ The following flags are supported: .PP .TS tab(@); -lw(18.3n) lw(27.1n) cw(9.5n) cw(15.1n). +lw(17.1n) lw(25.3n) lw(4.7n) cw(8.8n) cw(14.1n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -1179,6 +1477,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -1187,6 +1487,8 @@ T{ T}@T{ Maximum number of pages to return (1\-100) T}@T{ +int +T}@T{ \f[CR]0\f[R] T}@T{ \f[I]none\f[R] @@ -1209,12 +1511,14 @@ The following flags are supported: .PP .TS tab(@); -lw(20.9n) lw(20.9n) cw(10.8n) cw(17.3n). +lw(19.3n) lw(19.3n) lw(5.3n) cw(10.0n) cw(16.0n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -1225,6 +1529,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -1254,12 +1560,14 @@ The following flags are supported: .PP .TS tab(@); -lw(17.1n) lw(30.0n) cw(8.8n) cw(14.1n). +lw(16.0n) lw(28.1n) lw(4.4n) cw(8.3n) cw(13.2n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -1270,6 +1578,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -1278,6 +1588,8 @@ T{ T}@T{ Maximum number of notifications to return (1\-100) T}@T{ +int +T}@T{ \f[CR]0\f[R] T}@T{ \f[I]none\f[R] @@ -1300,12 +1612,14 @@ The following flags are supported: .PP .TS tab(@); -lw(20.9n) lw(20.9n) cw(10.8n) cw(17.3n). +lw(19.3n) lw(19.3n) lw(5.3n) cw(10.0n) cw(16.0n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -1316,6 +1630,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -1341,12 +1657,14 @@ The following flags are supported: .PP .TS tab(@); -lw(18.8n) lw(18.8n) cw(16.9n) cw(15.6n). +lw(17.2n) lw(17.2n) lw(4.7n) cw(16.6n) cw(14.2n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -1357,7 +1675,9 @@ T{ T}@T{ The configuration file T}@T{ -\f[CR]config.openstatus.yaml\f[R] +string +T}@T{ +\f[CR]\(dqconfig.openstatus.yaml\(dq\f[R] T}@T{ \f[I]none\f[R] T} @@ -1366,6 +1686,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -1390,12 +1712,14 @@ The following flags are supported: .PP .TS tab(@); -lw(20.9n) lw(20.9n) cw(10.8n) cw(17.3n). +lw(19.3n) lw(19.3n) lw(5.3n) cw(10.0n) cw(16.0n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -1406,6 +1730,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -1462,12 +1788,14 @@ The following flags are supported: .PP .TS tab(@); -lw(16.8n) lw(23.7n) cw(15.6n) cw(13.9n). +lw(15.4n) lw(22.3n) lw(4.2n) cw(15.4n) cw(12.7n). T{ Name T}@T{ Description T}@T{ +Type +T}@T{ Default value T}@T{ Environment variables @@ -1478,6 +1806,8 @@ T{ T}@T{ OpenStatus API Access Token T}@T{ +string +T}@T{ T}@T{ \f[CR]OPENSTATUS_API_TOKEN\f[R] T} @@ -1486,7 +1816,20 @@ T{ T}@T{ Directory to write Terraform files into T}@T{ -\f[CR]./openstatus\-terraform/\f[R] +string +T}@T{ +\f[CR]\(dq./openstatus\-terraform/\(dq\f[R] +T}@T{ +\f[I]none\f[R] +T} +T{ +\f[CR]\-\-force\f[R] (\f[CR]\-f\f[R]) +T}@T{ +Overwrite existing files in \(enoutput\-dir +T}@T{ +bool +T}@T{ +\f[CR]false\f[R] T}@T{ \f[I]none\f[R] T} diff --git a/go.mod b/go.mod index effb4b8..9430146 100644 --- a/go.mod +++ b/go.mod @@ -1,59 +1,63 @@ module github.com/openstatusHQ/cli -go 1.25 +go 1.25.0 -require github.com/urfave/cli/v3 v3.0.0-alpha9.2 // direct +require github.com/urfave/cli/v3 v3.9.0 // direct require ( - buf.build/gen/go/openstatus/api/connectrpc/gosimple v1.19.2-20260505152507-ee6c0b5379e7.1 - buf.build/gen/go/openstatus/api/protocolbuffers/go v1.36.11-20260505152507-ee6c0b5379e7.1 + buf.build/gen/go/openstatus/api/connectrpc/gosimple v1.19.2-20260512200453-7d7b7047611f.1 + buf.build/gen/go/openstatus/api/protocolbuffers/go v1.36.11-20260512200453-7d7b7047611f.1 connectrpc.com/connect v1.19.2 github.com/briandowns/spinner v1.23.2 github.com/charmbracelet/huh v1.0.0 - github.com/fatih/color v1.18.0 + github.com/fatih/color v1.19.0 github.com/google/go-cmp v0.7.0 github.com/hashicorp/hcl/v2 v2.24.0 github.com/joho/godotenv v1.5.1 - github.com/knadh/koanf/parsers/yaml v0.1.0 - github.com/knadh/koanf/providers/file v1.1.2 - github.com/knadh/koanf/v2 v2.1.1 + github.com/knadh/koanf/parsers/yaml v1.1.0 + github.com/knadh/koanf/providers/file v1.2.1 + github.com/knadh/koanf/v2 v2.3.4 github.com/logrusorgru/aurora/v4 v4.0.0 - github.com/mattn/go-isatty v0.0.20 - github.com/olekukonko/tablewriter v1.0.7 - github.com/rodaine/table v1.3.0 - github.com/urfave/cli-docs/v3 v3.0.0-alpha6 - github.com/zclconf/go-cty v1.18.0 - golang.org/x/term v0.32.0 - golang.org/x/text v0.25.0 - sigs.k8s.io/yaml v1.4.0 + github.com/mattn/go-isatty v0.0.22 + github.com/olekukonko/tablewriter v1.1.4 + github.com/rodaine/table v1.3.1 + github.com/urfave/cli-docs/v3 v3.1.0 + github.com/zclconf/go-cty v1.18.1 + golang.org/x/term v0.43.0 + golang.org/x/text v0.37.0 + sigs.k8s.io/yaml v1.6.0 ) require ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 // indirect buf.build/gen/go/gnostic/gnostic/protocolbuffers/go v1.36.11-20230414000709-087bc8072ce4.1 // indirect - github.com/agext/levenshtein v1.2.1 // indirect + github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/catppuccin/go v0.3.0 // indirect - github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect - github.com/charmbracelet/bubbletea v1.3.6 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/charmbracelet/bubbles v1.0.0 // indirect + github.com/charmbracelet/bubbletea v1.3.10 // indirect + github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/lipgloss v1.1.0 // indirect - github.com/charmbracelet/x/ansi v0.9.3 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect - github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/charmbracelet/x/ansi v0.11.7 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect + github.com/charmbracelet/x/exp/strings v0.1.0 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect - github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-json v0.10.6 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/lucasb-eyer/go-colorful v1.4.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.23 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect @@ -61,15 +65,17 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect - github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 // indirect - github.com/olekukonko/ll v0.0.8 // indirect + github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect + github.com/olekukonko/errors v1.3.0 // indirect + github.com/olekukonko/ll v0.1.8 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.44.0 // indirect + golang.org/x/tools v0.45.0 // indirect google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fb6ee2d..feceae7 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,17 @@ -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1 h1:s6hzCXtND/ICdGPTMGk7C+/BFlr2Jg5GyH0NKf4XGXg= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260415201107-50325440f8f2.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= buf.build/gen/go/gnostic/gnostic/protocolbuffers/go v1.36.11-20230414000709-087bc8072ce4.1 h1:t8f+WWZ5WNrZaP5zrpWD8f1tKU7eelJMbIAT9FRX558= buf.build/gen/go/gnostic/gnostic/protocolbuffers/go v1.36.11-20230414000709-087bc8072ce4.1/go.mod h1:/t9AeRQQp2iNkiGHDLfHLW3SzNpYpNPGRZ+Ih8+SOUs= -buf.build/gen/go/openstatus/api/connectrpc/gosimple v1.19.2-20260505152507-ee6c0b5379e7.1 h1:d8BfHZWqseh5LI5Or77MOExFOQB6PvJqWQYrZ6E2stY= -buf.build/gen/go/openstatus/api/connectrpc/gosimple v1.19.2-20260505152507-ee6c0b5379e7.1/go.mod h1:ENvll+poGWfEPVEr6u0EWr+zKbVk+P2JjfoJ1KUvFoU= -buf.build/gen/go/openstatus/api/protocolbuffers/go v1.36.11-20260505152507-ee6c0b5379e7.1 h1:SA8oRs3V26timaNILiVJ7jydtg0lA7WjvkgdkWDrsO4= -buf.build/gen/go/openstatus/api/protocolbuffers/go v1.36.11-20260505152507-ee6c0b5379e7.1/go.mod h1:GRsD/In1AV3/RC92zWVAjBpj57hJIQm7Rph0rLOYYPg= +buf.build/gen/go/openstatus/api/connectrpc/gosimple v1.19.2-20260512200453-7d7b7047611f.1 h1:NCdo1Xp+waRtzNvXDE9Io5gqzvvMIn3zItVFsxxmBc0= +buf.build/gen/go/openstatus/api/connectrpc/gosimple v1.19.2-20260512200453-7d7b7047611f.1/go.mod h1:pUXTguUtnl60N+cwJnHkPmsdnt0WniHDO9V37KslI4U= +buf.build/gen/go/openstatus/api/protocolbuffers/go v1.36.11-20260512200453-7d7b7047611f.1 h1:l4scOgYz5A41nIYE/WglmiGdwlZNWruvuyahqhfAKSs= +buf.build/gen/go/openstatus/api/protocolbuffers/go v1.36.11-20260512200453-7d7b7047611f.1/go.mod h1:GRsD/In1AV3/RC92zWVAjBpj57hJIQm7Rph0rLOYYPg= connectrpc.com/connect v1.19.2 h1:McQ83FGdzL+t60peksi0gXC7MQ/iLKgLduAnThbM0mo= connectrpc.com/connect v1.19.2/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= -github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= @@ -24,55 +24,60 @@ github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2 github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= -github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= -github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= -github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= -github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc= +github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E= +github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= +github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= +github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= +github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= github.com/charmbracelet/huh v1.0.0 h1:wOnedH8G4qzJbmhftTqrpppyqHakl/zbbNdXIWJyIxw= github.com/charmbracelet/huh v1.0.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= -github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= -github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= -github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= +github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= -github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= -github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/exp/strings v0.1.0 h1:i69S2XI7uG1u4NLGeJPSYU++Nmjvpo9nwd6aoEm7gkA= +github.com/charmbracelet/x/exp/strings v0.1.0/go.mod h1:/ehtMPNh9K4odGFkqYJKpIYyePhdp1hLBRvyY4bWkH8= +github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= -github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= @@ -81,25 +86,28 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= -github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w= -github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY= -github.com/knadh/koanf/providers/file v1.1.2 h1:aCC36YGOgV5lTtAFz2qkgtWdeQsgfxUkxDOe+2nQY3w= -github.com/knadh/koanf/providers/file v1.1.2/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= -github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= -github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4= +github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg= +github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM= +github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= +github.com/knadh/koanf/v2 v2.3.4 h1:fnynNSDlujWE+v83hAp8wKr/cdoxHLO0629SN+U8Urc= +github.com/knadh/koanf/v2 v2.3.4/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/logrusorgru/aurora/v4 v4.0.0 h1:sRjfPpun/63iADiSvGGjgA1cAYegEWMPCJdUpJYn9JA= github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4= +github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw= +github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= @@ -114,63 +122,59 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6 h1:r3FaAI0NZK3hSmtTDrBVREhKULp8oUeqLT5Eyl2mSPo= -github.com/olekukonko/errors v0.0.0-20250405072817-4e6d85265da6/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.0.8 h1:sbGZ1Fx4QxJXEqL/6IG8GEFnYojUSQ45dJVwN2FH2fc= -github.com/olekukonko/ll v0.0.8/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g= -github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw= -github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= +github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= +github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR6U= +github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= +github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= +github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= +github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= +github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE= -github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU= +github.com/rodaine/table v1.3.1 h1:jBVgg1bEu5EzEdYSrwUUlQpayDtkvtTmgFS0FPAxOq8= +github.com/rodaine/table v1.3.1/go.mod h1:VYCJRCHa2DpD25uFALcB6hi5ECF3eEJQVhCXRjHgXc4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/urfave/cli-docs/v3 v3.0.0-alpha6 h1:w/l/N0xw1rO/aHRIGXJ0lDwwYFOzilup1qGvIytP3BI= -github.com/urfave/cli-docs/v3 v3.0.0-alpha6/go.mod h1:p7Z4lg8FSTrPB9GTaNyTrK3ygffHZcK3w0cU2VE+mzU= -github.com/urfave/cli/v3 v3.0.0-alpha9.2 h1:CL8llQj3dGRLVQQzHxS+ZYRLanOuhyK1fXgLKD+qV+Y= -github.com/urfave/cli/v3 v3.0.0-alpha9.2/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= +github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= +github.com/urfave/cli/v3 v3.9.0 h1:AV9lIiPv3ukYnxunaCUsHnEozptYmDN2F0+yWqLMn/c= +github.com/urfave/cli/v3 v3.9.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/zclconf/go-cty v1.18.0 h1:pJ8+HNI4gFoyRNqVE37wWbJWVw43BZczFo7KUoRczaA= -github.com/zclconf/go-cty v1.18.0/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= +github.com/zclconf/go-cty v1.18.1 h1:yEGE8M4iIZlyKQURZNb2SnEyZlZHUcBCnx6KF81KuwM= +github.com/zclconf/go-cty v1.18.1/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= -golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/api/client.go b/internal/api/client.go index 5a276a4..360999d 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -7,8 +7,8 @@ import ( "os" "time" - output "github.com/openstatusHQ/cli/internal/cli" "connectrpc.com/connect" + output "github.com/openstatusHQ/cli/internal/cli" ) const APIBaseURL = "https://api.openstatus.dev/v1" diff --git a/internal/auth/auth.go b/internal/auth/auth.go index cc27009..63ac13e 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -89,4 +89,3 @@ func RemoveToken() error { } return nil } - diff --git a/internal/cli/output.go b/internal/cli/output.go index 5ec6419..f0a2a93 100644 --- a/internal/cli/output.go +++ b/internal/cli/output.go @@ -17,13 +17,15 @@ var ( debugMode atomic.Bool ) -func SetJSONOutput(v bool) { jsonOutput.Store(v) } -func SetQuietMode(v bool) { quietMode.Store(v) } -func SetDebugMode(v bool) { debugMode.Store(v) } -func IsJSONOutput() bool { return jsonOutput.Load() } -func IsQuiet() bool { return quietMode.Load() } -func IsDebug() bool { return debugMode.Load() } -func IsTerminal() bool { return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) } +func SetJSONOutput(v bool) { jsonOutput.Store(v) } +func SetQuietMode(v bool) { quietMode.Store(v) } +func SetDebugMode(v bool) { debugMode.Store(v) } +func IsJSONOutput() bool { return jsonOutput.Load() } +func IsQuiet() bool { return quietMode.Load() } +func IsDebug() bool { return debugMode.Load() } +func IsTerminal() bool { + return isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) +} func IsStdinTerminal() bool { return isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) } diff --git a/internal/cmd/app.go b/internal/cmd/app.go index 2684598..e8f1880 100644 --- a/internal/cmd/app.go +++ b/internal/cmd/app.go @@ -24,7 +24,7 @@ func NewApp() *cli.Command { Name: "openstatus", Suggest: true, EnableShellCompletion: true, - Usage: "Manage status pages, monitors, and incidents from the terminal", + Usage: "Manage status pages, monitors, and incidents from the terminal", Description: `OpenStatus CLI lets you manage your status pages and uptime monitors from the command line. Report and track incidents, define monitors as code, and run on-demand checks. @@ -40,7 +40,7 @@ Get started: openstatus run Run synthetic tests https://docs.openstatus.dev | https://github.com/openstatusHQ/cli/issues/new`, - Version: "v1.0.5", + Version: "v1.1.0", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "json", @@ -60,12 +60,12 @@ https://docs.openstatus.dev | https://github.com/openstatusHQ/cli/issues/new`, Usage: "Enable debug output", }, }, - Before: func(ctx context.Context, cmd *cli.Command) error { + Before: func(ctx context.Context, cmd *cli.Command) (context.Context, error) { output.SetJSONOutput(cmd.Bool("json")) output.SetQuietMode(cmd.Bool("quiet")) output.SetDebugMode(cmd.Bool("debug")) output.InitColorSettings(cmd.Bool("no-color")) - return nil + return ctx, nil }, Commands: []*cli.Command{ monitors.MonitorsCmd(), diff --git a/internal/cmd/app_test.go b/internal/cmd/app_test.go index 1d572dc..204e3c1 100644 --- a/internal/cmd/app_test.go +++ b/internal/cmd/app_test.go @@ -20,8 +20,8 @@ func Test_NewApp(t *testing.T) { t.Errorf("Expected app name 'openstatus', got %s", app.Name) } - if app.Version != "v1.0.5" { - t.Errorf("Expected version 'v1.0.5', got %s", app.Version) + if app.Version != "v1.1.0" { + t.Errorf("Expected version 'v1.1.0', got %s", app.Version) } if !app.Suggest { diff --git a/internal/config/monitor.go b/internal/config/monitor.go index 184450f..21ba1aa 100644 --- a/internal/config/monitor.go +++ b/internal/config/monitor.go @@ -57,7 +57,7 @@ type Request struct { } type OpenTelemetryConfig struct { - Endpoint string `json:"endpoint,omitempty" ,yaml:"endpoint,omitempty"` + Endpoint string `json:"endpoint,omitempty" ,yaml:"endpoint,omitempty"` Headers map[string]string `json:"headers,omitempty" ,yaml:"headers,omitempty"` } diff --git a/internal/maintenance/maintenance_create.go b/internal/maintenance/maintenance_create.go index 3e5b57d..4d6c691 100644 --- a/internal/maintenance/maintenance_create.go +++ b/internal/maintenance/maintenance_create.go @@ -6,8 +6,8 @@ import ( "net/http" "strings" - maintenancev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/maintenance/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/maintenance/v1/maintenancev1connect" + maintenancev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/maintenance/v1" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" "github.com/urfave/cli/v3" diff --git a/internal/maintenance/maintenance_delete.go b/internal/maintenance/maintenance_delete.go index d369530..2e651fb 100644 --- a/internal/maintenance/maintenance_delete.go +++ b/internal/maintenance/maintenance_delete.go @@ -6,8 +6,8 @@ import ( "net/http" "os" - maintenancev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/maintenance/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/maintenance/v1/maintenancev1connect" + maintenancev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/maintenance/v1" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" "github.com/urfave/cli/v3" @@ -38,8 +38,8 @@ func DeleteMaintenanceWithHTTPClient(ctx context.Context, httpClient *http.Clien func GetMaintenanceDeleteCmd() *cli.Command { return &cli.Command{ - Name: "delete", - Usage: "Delete a maintenance window", + Name: "delete", + Usage: "Delete a maintenance window", UsageText: `openstatus maintenance delete openstatus maintenance delete 12345 -y`, Flags: []cli.Flag{ diff --git a/internal/maintenance/maintenance_info.go b/internal/maintenance/maintenance_info.go index 20af563..0ff310e 100644 --- a/internal/maintenance/maintenance_info.go +++ b/internal/maintenance/maintenance_info.go @@ -7,14 +7,14 @@ import ( "os" "strings" - maintenancev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/maintenance/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/maintenance/v1/maintenancev1connect" - "github.com/openstatusHQ/cli/internal/auth" - output "github.com/openstatusHQ/cli/internal/cli" + maintenancev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/maintenance/v1" "github.com/logrusorgru/aurora/v4" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" + "github.com/openstatusHQ/cli/internal/auth" + output "github.com/openstatusHQ/cli/internal/cli" "github.com/urfave/cli/v3" ) diff --git a/internal/maintenance/maintenance_list.go b/internal/maintenance/maintenance_list.go index 65be76d..7905a54 100644 --- a/internal/maintenance/maintenance_list.go +++ b/internal/maintenance/maintenance_list.go @@ -5,8 +5,8 @@ import ( "fmt" "net/http" - maintenancev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/maintenance/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/maintenance/v1/maintenancev1connect" + maintenancev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/maintenance/v1" "github.com/fatih/color" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" diff --git a/internal/maintenance/maintenance_update.go b/internal/maintenance/maintenance_update.go index 8e73d3c..befbb5d 100644 --- a/internal/maintenance/maintenance_update.go +++ b/internal/maintenance/maintenance_update.go @@ -7,8 +7,8 @@ import ( "os" "strings" - maintenancev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/maintenance/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/maintenance/v1/maintenancev1connect" + maintenancev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/maintenance/v1" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" "github.com/urfave/cli/v3" diff --git a/internal/monitors/monitor_create.go b/internal/monitors/monitor_create.go index c5337fa..7417775 100644 --- a/internal/monitors/monitor_create.go +++ b/internal/monitors/monitor_create.go @@ -8,8 +8,8 @@ import ( "os" "strconv" - monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/monitor/v1/monitorv1connect" + monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "github.com/openstatusHQ/cli/internal/api" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" @@ -146,7 +146,7 @@ func GetMonitorCreateCmd() *cli.Command { Hidden: true, HideHelp: true, HideHelpCommand: true, - Description: "Create the monitors defined in the openstatus.yaml file", + Description: "Create the monitors defined in the openstatus.yaml file", UsageText: `openstatus monitors create openstatus monitors create --config custom.yaml -y`, diff --git a/internal/monitors/monitor_delete.go b/internal/monitors/monitor_delete.go index da2c01e..a8042a3 100644 --- a/internal/monitors/monitor_delete.go +++ b/internal/monitors/monitor_delete.go @@ -6,8 +6,8 @@ import ( "net/http" "os" - monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/monitor/v1/monitorv1connect" + monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" "github.com/urfave/cli/v3" diff --git a/internal/monitors/monitor_import.go b/internal/monitors/monitor_import.go index d1a4909..a7d8c74 100644 --- a/internal/monitors/monitor_import.go +++ b/internal/monitors/monitor_import.go @@ -8,8 +8,8 @@ import ( "os" "strconv" - monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/monitor/v1/monitorv1connect" + monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" "github.com/openstatusHQ/cli/internal/config" @@ -276,8 +276,8 @@ func ExportMonitorWithHTTPClient(ctx context.Context, httpClient *http.Client, a func GetMonitorImportCmd() *cli.Command { monitorImportCmd := cli.Command{ - Name: "import", - Usage: "Import all your monitors", + Name: "import", + Usage: "Import all your monitors", UsageText: `openstatus monitors import openstatus monitors import --output monitors.yaml`, Description: "Import all your monitors from your workspace to a YAML file; it will also create a lock file to manage your monitors with 'apply'.", diff --git a/internal/monitors/monitor_import_test.go b/internal/monitors/monitor_import_test.go index 00c8f47..7398c29 100644 --- a/internal/monitors/monitor_import_test.go +++ b/internal/monitors/monitor_import_test.go @@ -42,7 +42,7 @@ func Test_ExportMonitor(t *testing.T) { defer os.Remove("openstatus.lock") outputFile.Close() - err = monitors.ExportMonitorWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-api-key", outputFile.Name()) + err = monitors.ExportMonitorWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-api-key", outputFile.Name()) if err != nil { t.Fatalf("Expected no error, got %v", err) } @@ -81,7 +81,7 @@ func Test_ExportMonitor(t *testing.T) { defer os.Remove("openstatus.lock") outputFile.Close() - err = monitors.ExportMonitorWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-api-key", outputFile.Name()) + err = monitors.ExportMonitorWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-api-key", outputFile.Name()) if err != nil { t.Fatalf("Expected no error, got %v", err) } @@ -103,7 +103,7 @@ func Test_ExportMonitor(t *testing.T) { }, } - err := monitors.ExportMonitorWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"invalid-key", "output.yaml") + err := monitors.ExportMonitorWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "invalid-key", "output.yaml") if err == nil { t.Error("Expected error for non-200 status, got nil") } @@ -133,7 +133,7 @@ func Test_ExportMonitor(t *testing.T) { defer os.Remove("openstatus.lock") outputFile.Close() - err = monitors.ExportMonitorWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-api-key", outputFile.Name()) + err = monitors.ExportMonitorWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-api-key", outputFile.Name()) if err != nil { t.Fatalf("Expected no error, got %v", err) } diff --git a/internal/monitors/monitor_info_test.go b/internal/monitors/monitor_info_test.go index ee76edb..abfa73e 100644 --- a/internal/monitors/monitor_info_test.go +++ b/internal/monitors/monitor_info_test.go @@ -196,4 +196,3 @@ func Test_getMonitorInfo(t *testing.T) { } }) } - diff --git a/internal/monitors/monitor_log_info.go b/internal/monitors/monitor_log_info.go index 5b76185..e469462 100644 --- a/internal/monitors/monitor_log_info.go +++ b/internal/monitors/monitor_log_info.go @@ -9,8 +9,8 @@ import ( "sort" "strings" - monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/monitor/v1/monitorv1connect" + monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "github.com/fatih/color" "github.com/logrusorgru/aurora/v4" "github.com/openstatusHQ/cli/internal/auth" diff --git a/internal/monitors/monitor_logs.go b/internal/monitors/monitor_logs.go index e21dc98..de874ce 100644 --- a/internal/monitors/monitor_logs.go +++ b/internal/monitors/monitor_logs.go @@ -7,8 +7,8 @@ import ( "os" "time" - monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/monitor/v1/monitorv1connect" + monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "github.com/fatih/color" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" diff --git a/internal/monitors/monitor_trigger.go b/internal/monitors/monitor_trigger.go index 3ee9f8d..d411d5b 100644 --- a/internal/monitors/monitor_trigger.go +++ b/internal/monitors/monitor_trigger.go @@ -6,8 +6,8 @@ import ( "net/http" "os" - monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/monitor/v1/monitorv1connect" + monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" "github.com/urfave/cli/v3" @@ -46,8 +46,8 @@ func TriggerMonitorWithHTTPClient(ctx context.Context, httpClient *http.Client, func GetMonitorsTriggerCmd() *cli.Command { monitorsCmd := cli.Command{ - Name: "trigger", - Usage: "Trigger a monitor execution", + Name: "trigger", + Usage: "Trigger a monitor execution", UsageText: `openstatus monitors trigger openstatus monitors trigger 12345`, Description: "Trigger a monitor execution on demand. This command allows you to launch your tests on demand.", diff --git a/internal/monitors/monitor_update.go b/internal/monitors/monitor_update.go index f788095..9d700e6 100644 --- a/internal/monitors/monitor_update.go +++ b/internal/monitors/monitor_update.go @@ -6,8 +6,8 @@ import ( "net/http" "strconv" - monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/monitor/v1/monitorv1connect" + monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "github.com/openstatusHQ/cli/internal/config" ) diff --git a/internal/monitors/monitors.go b/internal/monitors/monitors.go index db678b1..c498e58 100644 --- a/internal/monitors/monitors.go +++ b/internal/monitors/monitors.go @@ -6,8 +6,8 @@ import ( "net/http" "strings" - monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/monitor/v1/monitorv1connect" + monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "connectrpc.com/connect" "github.com/openstatusHQ/cli/internal/api" "github.com/openstatusHQ/cli/internal/config" @@ -533,9 +533,9 @@ type TCPRunResult struct { func MonitorsCmd() *cli.Command { monitorsCmd := cli.Command{ - Name: "monitors", - Usage: "Manage your monitors", - Aliases: []string{"m"}, + Name: "monitors", + Usage: "Manage your monitors", + Aliases: []string{"m"}, Commands: []*cli.Command{ GetMonitorsApplyCmd(), GetMonitorCreateCmd(), diff --git a/internal/monitors/monitors_list.go b/internal/monitors/monitors_list.go index f0d75e8..37d3edd 100644 --- a/internal/monitors/monitors_list.go +++ b/internal/monitors/monitors_list.go @@ -5,11 +5,11 @@ import ( "fmt" "net/http" - monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/monitor/v1/monitorv1connect" + monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" + "github.com/fatih/color" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" - "github.com/fatih/color" "github.com/rodaine/table" "github.com/urfave/cli/v3" ) diff --git a/internal/statuspage/statuspage.go b/internal/statuspage/statuspage.go index 35c9e47..aa8dc30 100644 --- a/internal/statuspage/statuspage.go +++ b/internal/statuspage/statuspage.go @@ -3,8 +3,8 @@ package statuspage import ( "net/http" - status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_page/v1/status_pagev1connect" + status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "connectrpc.com/connect" "github.com/openstatusHQ/cli/internal/api" "github.com/urfave/cli/v3" diff --git a/internal/statuspage/statuspage_info.go b/internal/statuspage/statuspage_info.go index f14dbb9..8eb1b8e 100644 --- a/internal/statuspage/statuspage_info.go +++ b/internal/statuspage/statuspage_info.go @@ -9,8 +9,8 @@ import ( "strconv" "strings" - status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_page/v1/status_pagev1connect" + status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "github.com/fatih/color" "github.com/logrusorgru/aurora/v4" "github.com/olekukonko/tablewriter" diff --git a/internal/statuspage/statuspage_list.go b/internal/statuspage/statuspage_list.go index 6e996a0..0b50893 100644 --- a/internal/statuspage/statuspage_list.go +++ b/internal/statuspage/statuspage_list.go @@ -6,8 +6,8 @@ import ( "net/http" "strings" - status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_page/v1/status_pagev1connect" + status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "github.com/fatih/color" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" diff --git a/internal/statusreport/statusreport.go b/internal/statusreport/statusreport.go index 25b2baf..6b51a7c 100644 --- a/internal/statusreport/statusreport.go +++ b/internal/statusreport/statusreport.go @@ -4,8 +4,8 @@ import ( "fmt" "net/http" - status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_report/v1/status_reportv1connect" + status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "connectrpc.com/connect" "github.com/fatih/color" "github.com/openstatusHQ/cli/internal/api" diff --git a/internal/statusreport/statusreport_add_update.go b/internal/statusreport/statusreport_add_update.go index 3209f72..eecad66 100644 --- a/internal/statusreport/statusreport_add_update.go +++ b/internal/statusreport/statusreport_add_update.go @@ -8,8 +8,8 @@ import ( "strings" "time" - status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_report/v1/status_reportv1connect" + status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" "github.com/urfave/cli/v3" diff --git a/internal/statusreport/statusreport_add_update_test.go b/internal/statusreport/statusreport_add_update_test.go index 57e03ef..7806072 100644 --- a/internal/statusreport/statusreport_add_update_test.go +++ b/internal/statusreport/statusreport_add_update_test.go @@ -30,7 +30,7 @@ func Test_AddStatusReportUpdate(t *testing.T) { } err := statusreport.AddStatusReportUpdateWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "1", "identified", "Root cause found", "2026-03-20T10:30:00Z", false, ) if err != nil { @@ -55,7 +55,7 @@ func Test_AddStatusReportUpdate(t *testing.T) { } err := statusreport.AddStatusReportUpdateWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "1", "resolved", "Issue resolved", "", true, ) if err != nil { @@ -76,7 +76,7 @@ func Test_AddStatusReportUpdate(t *testing.T) { } err := statusreport.AddStatusReportUpdateWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "1", "invalid", "Message", "", false, ) if err == nil { @@ -97,7 +97,7 @@ func Test_AddStatusReportUpdate(t *testing.T) { } err := statusreport.AddStatusReportUpdateWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "", "investigating", "Message", "", false, ) if err == nil { @@ -122,7 +122,7 @@ func Test_AddStatusReportUpdate(t *testing.T) { } err := statusreport.AddStatusReportUpdateWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "999", "investigating", "Message", "", false, ) if err == nil { diff --git a/internal/statusreport/statusreport_create.go b/internal/statusreport/statusreport_create.go index 473b6c7..ee85fe2 100644 --- a/internal/statusreport/statusreport_create.go +++ b/internal/statusreport/statusreport_create.go @@ -7,8 +7,8 @@ import ( "strings" "time" - status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_report/v1/status_reportv1connect" + status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" "github.com/urfave/cli/v3" diff --git a/internal/statusreport/statusreport_create_test.go b/internal/statusreport/statusreport_create_test.go index 88c1c8e..2dcf008 100644 --- a/internal/statusreport/statusreport_create_test.go +++ b/internal/statusreport/statusreport_create_test.go @@ -33,7 +33,7 @@ func Test_CreateStatusReport(t *testing.T) { } id, err := statusreport.CreateStatusReportWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "API Outage", "investigating", "Investigating the issue", "2026-03-20T10:00:00Z", "page-1", nil, false, ) @@ -62,7 +62,7 @@ func Test_CreateStatusReport(t *testing.T) { } id, err := statusreport.CreateStatusReportWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "DB Issue", "investigating", "Looking into DB issues", "2026-03-20T10:00:00Z", "page-1", []string{"c1", "c2"}, true, ) @@ -87,7 +87,7 @@ func Test_CreateStatusReport(t *testing.T) { } _, err := statusreport.CreateStatusReportWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "Title", "invalid-status", "Message", "2026-03-20T10:00:00Z", "page-1", nil, false, ) @@ -113,7 +113,7 @@ func Test_CreateStatusReport(t *testing.T) { } _, err := statusreport.CreateStatusReportWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "Title", "investigating", "Message", "2026-03-20T10:00:00Z", "page-1", nil, false, ) diff --git a/internal/statusreport/statusreport_delete.go b/internal/statusreport/statusreport_delete.go index 830a476..f63e34c 100644 --- a/internal/statusreport/statusreport_delete.go +++ b/internal/statusreport/statusreport_delete.go @@ -6,8 +6,8 @@ import ( "net/http" "os" - status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_report/v1/status_reportv1connect" + status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" "github.com/urfave/cli/v3" @@ -38,8 +38,8 @@ func DeleteStatusReportWithHTTPClient(ctx context.Context, httpClient *http.Clie func GetStatusReportDeleteCmd() *cli.Command { return &cli.Command{ - Name: "delete", - Usage: "Delete a status report", + Name: "delete", + Usage: "Delete a status report", UsageText: `openstatus status-report delete openstatus status-report delete 12345 -y`, Flags: []cli.Flag{ diff --git a/internal/statusreport/statusreport_delete_test.go b/internal/statusreport/statusreport_delete_test.go index ca0c1f2..5165eee 100644 --- a/internal/statusreport/statusreport_delete_test.go +++ b/internal/statusreport/statusreport_delete_test.go @@ -25,7 +25,7 @@ func Test_DeleteStatusReport(t *testing.T) { }, } - err := statusreport.DeleteStatusReportWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-token", "") + err := statusreport.DeleteStatusReportWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-token", "") if err == nil { t.Error("Expected error for empty report ID, got nil") } @@ -56,7 +56,7 @@ func Test_DeleteStatusReport(t *testing.T) { }, } - err := statusreport.DeleteStatusReportWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-token", "123") + err := statusreport.DeleteStatusReportWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-token", "123") if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -78,7 +78,7 @@ func Test_DeleteStatusReport(t *testing.T) { }, } - err := statusreport.DeleteStatusReportWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-token", "999") + err := statusreport.DeleteStatusReportWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-token", "999") if err == nil { t.Error("Expected error for not found, got nil") } diff --git a/internal/statusreport/statusreport_info.go b/internal/statusreport/statusreport_info.go index 0c255c9..451130a 100644 --- a/internal/statusreport/statusreport_info.go +++ b/internal/statusreport/statusreport_info.go @@ -7,14 +7,14 @@ import ( "os" "strings" - status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_report/v1/status_reportv1connect" - "github.com/openstatusHQ/cli/internal/auth" - output "github.com/openstatusHQ/cli/internal/cli" + status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "github.com/logrusorgru/aurora/v4" "github.com/olekukonko/tablewriter" "github.com/olekukonko/tablewriter/renderer" "github.com/olekukonko/tablewriter/tw" + "github.com/openstatusHQ/cli/internal/auth" + output "github.com/openstatusHQ/cli/internal/cli" "github.com/urfave/cli/v3" ) diff --git a/internal/statusreport/statusreport_info_test.go b/internal/statusreport/statusreport_info_test.go index 9b1a5ee..2016fc9 100644 --- a/internal/statusreport/statusreport_info_test.go +++ b/internal/statusreport/statusreport_info_test.go @@ -29,7 +29,7 @@ func Test_GetStatusReportInfo(t *testing.T) { }, } - err := statusreport.GetStatusReportInfoWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-token", "1") + err := statusreport.GetStatusReportInfoWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-token", "1") if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -51,7 +51,7 @@ func Test_GetStatusReportInfo(t *testing.T) { }, } - err := statusreport.GetStatusReportInfoWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-token", "2") + err := statusreport.GetStatusReportInfoWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-token", "2") if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -69,7 +69,7 @@ func Test_GetStatusReportInfo(t *testing.T) { }, } - err := statusreport.GetStatusReportInfoWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-token", "") + err := statusreport.GetStatusReportInfoWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-token", "") if err == nil { t.Error("Expected error for empty report ID, got nil") } @@ -94,7 +94,7 @@ func Test_GetStatusReportInfo(t *testing.T) { }, } - err := statusreport.GetStatusReportInfoWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-token", "999") + err := statusreport.GetStatusReportInfoWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-token", "999") if err == nil { t.Error("Expected error, got nil") } diff --git a/internal/statusreport/statusreport_list.go b/internal/statusreport/statusreport_list.go index 089b427..6316f1a 100644 --- a/internal/statusreport/statusreport_list.go +++ b/internal/statusreport/statusreport_list.go @@ -5,11 +5,11 @@ import ( "fmt" "net/http" - status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_report/v1/status_reportv1connect" + status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" + "github.com/fatih/color" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" - "github.com/fatih/color" "github.com/rodaine/table" "github.com/urfave/cli/v3" ) diff --git a/internal/statusreport/statusreport_list_test.go b/internal/statusreport/statusreport_list_test.go index 021de86..c542479 100644 --- a/internal/statusreport/statusreport_list_test.go +++ b/internal/statusreport/statusreport_list_test.go @@ -36,7 +36,7 @@ func Test_ListStatusReports(t *testing.T) { t.Cleanup(func() { log.SetOutput(os.Stdout) }) - err := statusreport.ListStatusReportsWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-token", "", 0) + err := statusreport.ListStatusReportsWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-token", "", 0) if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -58,7 +58,7 @@ func Test_ListStatusReports(t *testing.T) { }, } - err := statusreport.ListStatusReportsWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-token", "", 0) + err := statusreport.ListStatusReportsWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-token", "", 0) if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -80,7 +80,7 @@ func Test_ListStatusReports(t *testing.T) { }, } - err := statusreport.ListStatusReportsWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-token", "investigating", 0) + err := statusreport.ListStatusReportsWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-token", "investigating", 0) if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -98,7 +98,7 @@ func Test_ListStatusReports(t *testing.T) { }, } - err := statusreport.ListStatusReportsWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-token", "invalid", 0) + err := statusreport.ListStatusReportsWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-token", "invalid", 0) if err == nil { t.Error("Expected error for invalid status, got nil") } @@ -120,7 +120,7 @@ func Test_ListStatusReports(t *testing.T) { }, } - err := statusreport.ListStatusReportsWithHTTPClient(context.Background(), interceptor.GetHTTPClient(),"test-token", "", 0) + err := statusreport.ListStatusReportsWithHTTPClient(context.Background(), interceptor.GetHTTPClient(), "test-token", "", 0) if err == nil { t.Error("Expected error, got nil") } diff --git a/internal/statusreport/statusreport_update.go b/internal/statusreport/statusreport_update.go index 8385bc7..1104c42 100644 --- a/internal/statusreport/statusreport_update.go +++ b/internal/statusreport/statusreport_update.go @@ -7,8 +7,8 @@ import ( "os" "strings" - status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_report/v1/status_reportv1connect" + status_reportv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_report/v1" "github.com/openstatusHQ/cli/internal/auth" output "github.com/openstatusHQ/cli/internal/cli" "github.com/urfave/cli/v3" diff --git a/internal/statusreport/statusreport_update_test.go b/internal/statusreport/statusreport_update_test.go index 4606f6b..f9dc2e0 100644 --- a/internal/statusreport/statusreport_update_test.go +++ b/internal/statusreport/statusreport_update_test.go @@ -30,7 +30,7 @@ func Test_UpdateStatusReport(t *testing.T) { } err := statusreport.UpdateStatusReportWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "1", "New Title", nil, true, false, ) if err != nil { @@ -55,7 +55,7 @@ func Test_UpdateStatusReport(t *testing.T) { } err := statusreport.UpdateStatusReportWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "1", "", []string{"c1", "c2"}, false, true, ) if err != nil { @@ -80,7 +80,7 @@ func Test_UpdateStatusReport(t *testing.T) { } err := statusreport.UpdateStatusReportWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "1", "Updated Title", []string{"c3"}, true, true, ) if err != nil { @@ -101,7 +101,7 @@ func Test_UpdateStatusReport(t *testing.T) { } err := statusreport.UpdateStatusReportWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "1", "", nil, false, false, ) if err == nil { @@ -125,7 +125,7 @@ func Test_UpdateStatusReport(t *testing.T) { } err := statusreport.UpdateStatusReportWithHTTPClient( - context.Background(), interceptor.GetHTTPClient(),"test-token", + context.Background(), interceptor.GetHTTPClient(), "test-token", "", "Title", nil, true, false, ) if err == nil { diff --git a/internal/statusreport/statusreport_wizard.go b/internal/statusreport/statusreport_wizard.go index bc0af39..3f4c334 100644 --- a/internal/statusreport/statusreport_wizard.go +++ b/internal/statusreport/statusreport_wizard.go @@ -334,4 +334,3 @@ func runAddUpdateWizard(ctx context.Context, apiKey string, prefilled *addUpdate return &inputs, nil } - diff --git a/internal/terraform/cli_test.go b/internal/terraform/cli_test.go new file mode 100644 index 0000000..e6ecfbd --- /dev/null +++ b/internal/terraform/cli_test.go @@ -0,0 +1,84 @@ +package terraform + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestCheckExistingFiles_RefusesExisting(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "monitors.tf"), []byte("existing"), 0644); err != nil { + t.Fatalf("seeding fixture: %v", err) + } + + err := checkExistingFiles(dir, false) + if err == nil { + t.Fatal("expected error, got nil") + } + if !strings.Contains(err.Error(), "monitors.tf") { + t.Errorf("expected error to mention filename, got: %v", err) + } + if !strings.Contains(err.Error(), "--force") { + t.Errorf("expected error to mention --force, got: %v", err) + } +} + +func TestCheckExistingFiles_OverwritesWithForce(t *testing.T) { + dir := t.TempDir() + if err := os.WriteFile(filepath.Join(dir, "monitors.tf"), []byte("existing"), 0644); err != nil { + t.Fatalf("seeding fixture: %v", err) + } + + if err := checkExistingFiles(dir, true); err != nil { + t.Errorf("expected nil with force=true, got: %v", err) + } +} + +func TestCheckExistingFiles_EmptyDir(t *testing.T) { + dir := t.TempDir() + if err := checkExistingFiles(dir, false); err != nil { + t.Errorf("expected nil for empty dir, got: %v", err) + } +} + +func TestCheckExistingFiles_NonexistentDir(t *testing.T) { + if err := checkExistingFiles(filepath.Join(t.TempDir(), "does-not-exist"), false); err != nil { + t.Errorf("expected nil for nonexistent dir, got: %v", err) + } +} + +func TestPrintSummary_IncludesInitUpgradeHint(t *testing.T) { + out := captureStdout(t, func() { + printSummary("/tmp/out", &WorkspaceData{}) + }) + if !strings.Contains(out, "terraform init -upgrade") { + t.Errorf("expected init-upgrade hint, got:\n%s", out) + } + if !strings.Contains(out, "~> 0.2") { + t.Errorf("expected version mention in hint, got:\n%s", out) + } +} + +func captureStdout(t *testing.T, fn func()) string { + t.Helper() + orig := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("pipe: %v", err) + } + os.Stdout = w + defer func() { os.Stdout = orig }() + + done := make(chan string, 1) + go func() { + b, _ := io.ReadAll(r) + done <- string(b) + }() + + fn() + w.Close() + return <-done +} diff --git a/internal/terraform/enums.go b/internal/terraform/enums.go index 6bfd3e6..3bc9f3a 100644 --- a/internal/terraform/enums.go +++ b/internal/terraform/enums.go @@ -111,37 +111,6 @@ func recordComparatorToString(c monitorv1.RecordComparator) string { } } -func notificationProviderToString(p notificationv1.NotificationProvider) string { - switch p { - case notificationv1.NotificationProvider_NOTIFICATION_PROVIDER_DISCORD: - return "discord" - case notificationv1.NotificationProvider_NOTIFICATION_PROVIDER_EMAIL: - return "email" - case notificationv1.NotificationProvider_NOTIFICATION_PROVIDER_GOOGLE_CHAT: - return "google_chat" - case notificationv1.NotificationProvider_NOTIFICATION_PROVIDER_GRAFANA_ONCALL: - return "grafana_oncall" - case notificationv1.NotificationProvider_NOTIFICATION_PROVIDER_NTFY: - return "ntfy" - case notificationv1.NotificationProvider_NOTIFICATION_PROVIDER_PAGERDUTY: - return "pagerduty" - case notificationv1.NotificationProvider_NOTIFICATION_PROVIDER_OPSGENIE: - return "opsgenie" - case notificationv1.NotificationProvider_NOTIFICATION_PROVIDER_SLACK: - return "slack" - case notificationv1.NotificationProvider_NOTIFICATION_PROVIDER_SMS: - return "sms" - case notificationv1.NotificationProvider_NOTIFICATION_PROVIDER_TELEGRAM: - return "telegram" - case notificationv1.NotificationProvider_NOTIFICATION_PROVIDER_WEBHOOK: - return "webhook" - case notificationv1.NotificationProvider_NOTIFICATION_PROVIDER_WHATSAPP: - return "whatsapp" - default: - return "unknown" - } -} - func opsgenieRegionToString(r notificationv1.OpsgenieRegion) string { switch r { case notificationv1.OpsgenieRegion_OPSGENIE_REGION_US: @@ -172,7 +141,35 @@ func pageAccessTypeToString(t status_pagev1.PageAccessType) string { return "password" case status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_AUTHENTICATED: return "email-domain" + case status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_IP_RESTRICTED: + return "ip" default: return "public" } } + +func pageThemeToString(t status_pagev1.PageTheme) string { + switch t { + case status_pagev1.PageTheme_PAGE_THEME_SYSTEM: + return "system" + case status_pagev1.PageTheme_PAGE_THEME_LIGHT: + return "light" + case status_pagev1.PageTheme_PAGE_THEME_DARK: + return "dark" + default: + return "" + } +} + +func localeToString(l status_pagev1.Locale) string { + switch l { + case status_pagev1.Locale_LOCALE_EN: + return "en" + case status_pagev1.Locale_LOCALE_FR: + return "fr" + case status_pagev1.Locale_LOCALE_DE: + return "de" + default: + return "" + } +} diff --git a/internal/terraform/fetch.go b/internal/terraform/fetch.go index e7f5c11..a953cef 100644 --- a/internal/terraform/fetch.go +++ b/internal/terraform/fetch.go @@ -4,12 +4,12 @@ import ( "context" "fmt" - monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/monitor/v1/monitorv1connect" - notificationv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/notification/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/notification/v1/notificationv1connect" - status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_page/v1/status_pagev1connect" + monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" + notificationv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/notification/v1" + status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "connectrpc.com/connect" "github.com/openstatusHQ/cli/internal/api" ) diff --git a/internal/terraform/generate.go b/internal/terraform/generate.go index 9a6a915..d2fbf97 100644 --- a/internal/terraform/generate.go +++ b/internal/terraform/generate.go @@ -11,6 +11,14 @@ import ( "github.com/urfave/cli/v3" ) +var generatedFileNames = []string{ + "provider.tf", + "monitors.tf", + "notifications.tf", + "status_pages.tf", + "imports.tf", +} + func GetTerraformGenerateCmd() *cli.Command { return &cli.Command{ Name: "generate", @@ -30,6 +38,11 @@ func GetTerraformGenerateCmd() *cli.Command { Value: "./openstatus-terraform/", Aliases: []string{"o"}, }, + &cli.BoolFlag{ + Name: "force", + Usage: "Overwrite existing files in --output-dir", + Aliases: []string{"f"}, + }, }, Action: func(ctx context.Context, cmd *cli.Command) error { apiKey, err := auth.ResolveAccessToken(cmd) @@ -38,6 +51,9 @@ func GetTerraformGenerateCmd() *cli.Command { } outputDir := cmd.String("output-dir") + if err := checkExistingFiles(outputDir, cmd.Bool("force")); err != nil { + return cli.Exit(err.Error(), 1) + } if err := os.MkdirAll(outputDir, 0755); err != nil { return cli.Exit(fmt.Sprintf("failed to create output directory: %v", err), 1) } @@ -89,6 +105,18 @@ func GetTerraformGenerateCmd() *cli.Command { } } +func checkExistingFiles(outputDir string, force bool) error { + if force { + return nil + } + for _, name := range generatedFileNames { + if _, err := os.Stat(filepath.Join(outputDir, name)); err == nil { + return fmt.Errorf("refusing to overwrite existing file %s; pass --force to replace", name) + } + } + return nil +} + func writeFile(path string, content []byte) error { f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { @@ -131,4 +159,5 @@ func printSummary(outputDir string, data *WorkspaceData) { fmt.Printf(" cd %s\n", outputDir) fmt.Printf(" terraform init\n") fmt.Printf(" terraform plan\n") + fmt.Printf("\nNote: provider version pinned to ~> 0.2. Run 'terraform init -upgrade' if you previously ran this command.\n") } diff --git a/internal/terraform/generate_test.go b/internal/terraform/generate_test.go index be3d1e5..cf54094 100644 --- a/internal/terraform/generate_test.go +++ b/internal/terraform/generate_test.go @@ -12,7 +12,7 @@ import ( func TestGenerateProviderFile(t *testing.T) { content := string(GenerateProviderFile()) mustContain(t, content, `source = "openstatusHQ/openstatus"`) - mustContain(t, content, `version = "~> 0.1.0"`) + mustContain(t, content, `version = "~> 0.2"`) mustContain(t, content, `provider "openstatus" {}`) mustContain(t, content, `OPENSTATUS_API_TOKEN`) } @@ -213,6 +213,357 @@ func TestCrossReferenceFallbackToString(t *testing.T) { mustContain(t, content, `monitor_id = "nonexistent-id"`) } +func TestGenerateNotificationsFile_MsTeams(t *testing.T) { + n := ¬ificationv1.Notification{} + n.SetId("1") + n.SetName("Teams Alerts") + + nd := ¬ificationv1.NotificationData{} + td := ¬ificationv1.MsTeamsData{} + td.SetWebhookUrl("https://prod.example.com/webhook") + nd.SetMsTeams(td) + n.SetData(nd) + + data := &WorkspaceData{Notifications: []*notificationv1.Notification{n}} + gen := NewGenerator(data) + content := string(gen.GenerateNotificationsFile().Bytes()) + + mustContain(t, content, `resource "openstatus_notification" "teams_alerts"`) + mustContain(t, content, `provider_type = "ms_teams"`) + mustContain(t, content, "ms_teams {") + mustContain(t, content, `webhook_url = "https://prod.example.com/webhook"`) +} + +func TestGenerateNotificationsFile_WebhookHeaders(t *testing.T) { + n := ¬ificationv1.Notification{} + n.SetId("1") + n.SetName("Webhook") + + nd := ¬ificationv1.NotificationData{} + wd := ¬ificationv1.WebhookData{} + wd.SetEndpoint("https://example.com/hook") + h1 := ¬ificationv1.WebhookHeader{} + h1.SetKey("Authorization") + h1.SetValue("Bearer xyz") + h2 := ¬ificationv1.WebhookHeader{} + h2.SetKey("X-Source") + h2.SetValue("openstatus") + wd.SetHeaders([]*notificationv1.WebhookHeader{h1, h2}) + nd.SetWebhook(wd) + n.SetData(nd) + + data := &WorkspaceData{Notifications: []*notificationv1.Notification{n}} + gen := NewGenerator(data) + content := string(gen.GenerateNotificationsFile().Bytes()) + + mustContain(t, content, `headers = [`) + mustContain(t, content, `key = "Authorization"`) + mustContain(t, content, `value = "Bearer xyz"`) + mustContain(t, content, `key = "X-Source"`) + mustNotContain(t, content, "headers {") +} + +func TestGenerateNotificationsFile_MonitorIdsTraversal(t *testing.T) { + m := &monitorv1.HTTPMonitor{} + m.SetId("mon-known") + m.SetName("API") + + n := ¬ificationv1.Notification{} + n.SetId("n1") + n.SetName("Slack") + n.SetMonitorIds([]string{"mon-unknown", "mon-known"}) + + nd := ¬ificationv1.NotificationData{} + sd := ¬ificationv1.SlackData{} + sd.SetWebhookUrl("https://hooks.example.com/xxx") + nd.SetSlack(sd) + n.SetData(nd) + + data := &WorkspaceData{ + HTTPMonitors: []*monitorv1.HTTPMonitor{m}, + Notifications: []*notificationv1.Notification{n}, + } + gen := NewGenerator(data) + content := string(gen.GenerateNotificationsFile().Bytes()) + + mustContain(t, content, "openstatus_http_monitor.api.id") + mustContain(t, content, `"mon-unknown"`) + idx := strings.Index(content, "monitor_ids") + if idx == -1 { + t.Fatalf("monitor_ids not found in output:\n%s", content) + } + line := content[idx : strings.Index(content[idx:], "\n")+idx] + if strings.Index(line, "openstatus_http_monitor.api.id") > strings.Index(line, `"mon-unknown"`) { + t.Errorf("expected sorted order (mon-known first, mon-unknown second), got: %s", line) + } +} + +func TestGenerateNotificationsFile_UnknownProviderSkipped(t *testing.T) { + n := ¬ificationv1.Notification{} + n.SetId("n1") + n.SetName("Mystery") + + data := &WorkspaceData{Notifications: []*notificationv1.Notification{n}} + gen := NewGenerator(data) + nContent := string(gen.GenerateNotificationsFile().Bytes()) + importsContent := string(gen.GenerateImportsFile().Bytes()) + + if nContent != "" { + t.Errorf("expected empty notifications file for skipped resource, got:\n%s", nContent) + } + mustNotContain(t, importsContent, "openstatus_notification.mystery") + mustNotContain(t, importsContent, `id = "n1"`) +} + +func TestGenerateMonitorsFile_HTTP_OpenTelemetry(t *testing.T) { + ot := &monitorv1.OpenTelemetryConfig{} + ot.SetEndpoint("https://otel.example.com/v1/metrics") + h := &monitorv1.Headers{} + h.SetKey("X-Api-Key") + h.SetValue("secret") + ot.SetHeaders([]*monitorv1.Headers{h}) + + m := &monitorv1.HTTPMonitor{} + m.SetId("1") + m.SetName("API") + m.SetUrl("https://api.example.com") + m.SetPeriodicity(monitorv1.Periodicity_PERIODICITY_5M) + m.SetActive(true) + m.SetOpenTelemetry(ot) + + data := &WorkspaceData{HTTPMonitors: []*monitorv1.HTTPMonitor{m}} + gen := NewGenerator(data) + content := string(gen.GenerateMonitorsFile().Bytes()) + + mustContain(t, content, "open_telemetry {") + mustContain(t, content, `endpoint = "https://otel.example.com/v1/metrics"`) + mustContain(t, content, `key = "X-Api-Key"`) + mustContain(t, content, `value = "secret"`) +} + +func TestGenerateMonitorsFile_TCP_OpenTelemetry(t *testing.T) { + ot := &monitorv1.OpenTelemetryConfig{} + ot.SetEndpoint("https://otel.example.com/v1/metrics") + + m := &monitorv1.TCPMonitor{} + m.SetId("1") + m.SetName("TCP") + m.SetUri("example.com:443") + m.SetPeriodicity(monitorv1.Periodicity_PERIODICITY_5M) + m.SetActive(true) + m.SetOpenTelemetry(ot) + + data := &WorkspaceData{TCPMonitors: []*monitorv1.TCPMonitor{m}} + gen := NewGenerator(data) + content := string(gen.GenerateMonitorsFile().Bytes()) + + mustContain(t, content, "open_telemetry {") + mustContain(t, content, `endpoint = "https://otel.example.com/v1/metrics"`) +} + +func TestGenerateMonitorsFile_DNS_OpenTelemetry(t *testing.T) { + ot := &monitorv1.OpenTelemetryConfig{} + ot.SetEndpoint("https://otel.example.com/v1/metrics") + + m := &monitorv1.DNSMonitor{} + m.SetId("1") + m.SetName("DNS") + m.SetUri("example.com") + m.SetPeriodicity(monitorv1.Periodicity_PERIODICITY_5M) + m.SetActive(true) + m.SetOpenTelemetry(ot) + + data := &WorkspaceData{DNSMonitors: []*monitorv1.DNSMonitor{m}} + gen := NewGenerator(data) + content := string(gen.GenerateMonitorsFile().Bytes()) + + mustContain(t, content, "open_telemetry {") + mustContain(t, content, `endpoint = "https://otel.example.com/v1/metrics"`) +} + +func TestGenerateMonitorsFile_OpenTelemetry_SkippedWhenEmpty(t *testing.T) { + m := &monitorv1.HTTPMonitor{} + m.SetId("1") + m.SetName("API") + m.SetUrl("https://api.example.com") + m.SetPeriodicity(monitorv1.Periodicity_PERIODICITY_5M) + m.SetActive(true) + m.SetOpenTelemetry(&monitorv1.OpenTelemetryConfig{}) + + data := &WorkspaceData{HTTPMonitors: []*monitorv1.HTTPMonitor{m}} + gen := NewGenerator(data) + content := string(gen.GenerateMonitorsFile().Bytes()) + + mustNotContain(t, content, "open_telemetry") +} + +func TestGenerateMonitorsFile_RegionsSorted(t *testing.T) { + m := &monitorv1.HTTPMonitor{} + m.SetId("1") + m.SetName("API") + m.SetUrl("https://api.example.com") + m.SetPeriodicity(monitorv1.Periodicity_PERIODICITY_5M) + m.SetActive(true) + m.SetRegions([]monitorv1.Region{ + monitorv1.Region_REGION_FLY_SYD, + monitorv1.Region_REGION_FLY_AMS, + monitorv1.Region_REGION_FLY_IAD, + }) + + data := &WorkspaceData{HTTPMonitors: []*monitorv1.HTTPMonitor{m}} + gen := NewGenerator(data) + content := string(gen.GenerateMonitorsFile().Bytes()) + + mustContain(t, content, `["fly-ams", "fly-iad", "fly-syd"]`) +} + +func TestGenerateStatusPagesFile_ComponentGroupDefaultOpen(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Page") + page.SetSlug("page") + + grpOpen := &status_pagev1.PageComponentGroup{} + grpOpen.SetId("g1") + grpOpen.SetPageId("p1") + grpOpen.SetName("Infrastructure") + grpOpen.SetDefaultOpen(true) + + grpClosed := &status_pagev1.PageComponentGroup{} + grpClosed.SetId("g2") + grpClosed.SetPageId("p1") + grpClosed.SetName("Other") + + data := &WorkspaceData{ + StatusPages: []StatusPageData{{ + Page: page, + Groups: []*status_pagev1.PageComponentGroup{grpOpen, grpClosed}, + }}, + } + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustContain(t, content, `resource "openstatus_status_page_component_group" "infrastructure"`) + mustContain(t, content, `default_open = true`) + mustContain(t, content, `resource "openstatus_status_page_component_group" "other"`) + + otherStart := strings.Index(content, `"other"`) + if otherStart == -1 { + t.Fatalf("expected 'other' resource not found") + } + otherBlock := content[otherStart:] + if strings.Contains(otherBlock[:strings.Index(otherBlock, "}")], "default_open") { + t.Errorf("default_open should not be emitted for the 'other' group, got:\n%s", otherBlock) + } +} + +func TestGenerateStatusPagesFile_IPAccess(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Internal") + page.SetSlug("internal") + page.SetAccessType(status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_IP_RESTRICTED) + page.SetAllowedIpRanges("10.0.0.0/8,192.168.0.0/16") + + data := &WorkspaceData{StatusPages: []StatusPageData{{Page: page}}} + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustContain(t, content, `access_type = "ip"`) + mustContain(t, content, `allowed_ip_ranges = "10.0.0.0/8,192.168.0.0/16"`) + mustNotContain(t, content, "REPLACE_ME") +} + +func TestGenerateStatusPagesFile_IPAccessEmptyFallback(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Internal") + page.SetSlug("internal") + page.SetAccessType(status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_IP_RESTRICTED) + + data := &WorkspaceData{StatusPages: []StatusPageData{{Page: page}}} + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustContain(t, content, `access_type = "ip"`) + mustContain(t, content, `allowed_ip_ranges = "REPLACE_ME"`) + mustContain(t, content, "# TODO:") +} + +func TestGenerateStatusPagesFile_EmailDomainAccess(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Internal") + page.SetSlug("internal") + page.SetAccessType(status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_AUTHENTICATED) + page.SetAuthEmailDomains([]string{"example.com", "acme.com"}) + + data := &WorkspaceData{StatusPages: []StatusPageData{{Page: page}}} + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustContain(t, content, `access_type = "email-domain"`) + mustContain(t, content, `auth_email_domains = ["acme.com", "example.com"]`) + mustNotContain(t, content, "REPLACE_ME") +} + +func TestGenerateStatusPagesFile_EmailDomainEmptyFallback(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Internal") + page.SetSlug("internal") + page.SetAccessType(status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_AUTHENTICATED) + + data := &WorkspaceData{StatusPages: []StatusPageData{{Page: page}}} + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustContain(t, content, `access_type = "email-domain"`) + mustContain(t, content, `auth_email_domains = ["REPLACE_ME"]`) + mustContain(t, content, "# TODO:") +} + +func TestGenerateStatusPagesFile_ThemeLocaleAllowIndex(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Themed") + page.SetSlug("themed") + page.SetTheme(status_pagev1.PageTheme_PAGE_THEME_DARK) + page.SetDefaultLocale(status_pagev1.Locale_LOCALE_FR) + page.SetLocales([]status_pagev1.Locale{ + status_pagev1.Locale_LOCALE_FR, + status_pagev1.Locale_LOCALE_EN, + }) + page.SetAllowIndex(true) + + data := &WorkspaceData{StatusPages: []StatusPageData{{Page: page}}} + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustContain(t, content, `theme = "dark"`) + mustContain(t, content, `default_locale = "fr"`) + mustContain(t, content, `locales = ["en", "fr"]`) + mustContain(t, content, `allow_index = true`) +} + +func TestGenerateStatusPagesFile_DefaultsOmitted(t *testing.T) { + page := &status_pagev1.StatusPage{} + page.SetId("p1") + page.SetTitle("Plain") + page.SetSlug("plain") + page.SetTheme(status_pagev1.PageTheme_PAGE_THEME_SYSTEM) + page.SetDefaultLocale(status_pagev1.Locale_LOCALE_EN) + + data := &WorkspaceData{StatusPages: []StatusPageData{{Page: page}}} + gen := NewGenerator(data) + content := string(gen.GenerateStatusPagesFile().Bytes()) + + mustNotContain(t, content, "theme") + mustNotContain(t, content, "default_locale") + mustNotContain(t, content, "locales") + mustNotContain(t, content, "allow_index") +} + func mustContain(t *testing.T, content, substr string) { t.Helper() if !strings.Contains(content, substr) { diff --git a/internal/terraform/hcl.go b/internal/terraform/hcl.go index f6c84a8..b8d45e6 100644 --- a/internal/terraform/hcl.go +++ b/internal/terraform/hcl.go @@ -2,6 +2,8 @@ package terraform import ( "fmt" + "os" + "sort" monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" notificationv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/notification/v1" @@ -23,29 +25,31 @@ type Generator struct { pageRefs map[string]resourceRef groupRefs map[string]resourceRef - httpMonitorNames map[string]string - tcpMonitorNames map[string]string - dnsMonitorNames map[string]string - notifNames map[string]string - pageNames map[string]string - componentNames map[string]string - groupNames map[string]string + httpMonitorNames map[string]string + tcpMonitorNames map[string]string + dnsMonitorNames map[string]string + notifNames map[string]string + pageNames map[string]string + componentNames map[string]string + groupNames map[string]string + skippedNotifications map[string]bool } func NewGenerator(data *WorkspaceData) *Generator { g := &Generator{ - data: data, - registry: NewNameRegistry(), - monitorRefs: make(map[string]resourceRef), - pageRefs: make(map[string]resourceRef), - groupRefs: make(map[string]resourceRef), - httpMonitorNames: make(map[string]string), - tcpMonitorNames: make(map[string]string), - dnsMonitorNames: make(map[string]string), - notifNames: make(map[string]string), - pageNames: make(map[string]string), - componentNames: make(map[string]string), - groupNames: make(map[string]string), + data: data, + registry: NewNameRegistry(), + monitorRefs: make(map[string]resourceRef), + pageRefs: make(map[string]resourceRef), + groupRefs: make(map[string]resourceRef), + httpMonitorNames: make(map[string]string), + tcpMonitorNames: make(map[string]string), + dnsMonitorNames: make(map[string]string), + notifNames: make(map[string]string), + pageNames: make(map[string]string), + componentNames: make(map[string]string), + groupNames: make(map[string]string), + skippedNotifications: make(map[string]bool), } for _, m := range data.HTTPMonitors { @@ -64,6 +68,11 @@ func NewGenerator(data *WorkspaceData) *Generator { g.monitorRefs[m.GetId()] = resourceRef{"openstatus_dns_monitor", name} } for _, n := range data.Notifications { + if _, ok := renderableNotification(n); !ok { + g.skippedNotifications[n.GetId()] = true + fmt.Fprintf(os.Stderr, "warning: skipping notification %q — unknown provider type (CLI may be outdated)\n", n.GetName()) + continue + } name := g.registry.Name("openstatus_notification", n.GetName()) g.notifNames[n.GetId()] = name } @@ -92,7 +101,7 @@ func GenerateProviderFile() []byte { required_providers { openstatus = { source = "openstatusHQ/openstatus" - version = "~> 0.1.0" + version = "~> 0.2" } } } @@ -144,6 +153,7 @@ func (g *Generator) GenerateMonitorsFile() *hclwrite.File { writeStatusCodeAssertions(b, m.GetStatusCodeAssertions()) writeBodyAssertions(b, m.GetBodyAssertions()) writeHeaderAssertions(b, m.GetHeaderAssertions()) + writeOpenTelemetry(b, m.GetOpenTelemetry()) body.AppendNewline() } @@ -173,6 +183,7 @@ func (g *Generator) GenerateMonitorsFile() *hclwrite.File { } writeRegions(b, m.GetRegions()) + writeOpenTelemetry(b, m.GetOpenTelemetry()) body.AppendNewline() } @@ -209,6 +220,7 @@ func (g *Generator) GenerateMonitorsFile() *hclwrite.File { ab.SetAttributeValue("target", cty.StringVal(a.GetTarget())) ab.SetAttributeValue("comparator", cty.StringVal(recordComparatorToString(a.GetComparator()))) } + writeOpenTelemetry(b, m.GetOpenTelemetry()) body.AppendNewline() } @@ -221,21 +233,21 @@ func (g *Generator) GenerateNotificationsFile() *hclwrite.File { body := f.Body() for _, n := range g.data.Notifications { + if g.skippedNotifications[n.GetId()] { + continue + } + providerType, ok := renderableNotification(n) + if !ok { + continue + } name := g.notifNames[n.GetId()] block := body.AppendNewBlock("resource", []string{"openstatus_notification", name}) b := block.Body() b.SetAttributeValue("name", cty.StringVal(n.GetName())) - b.SetAttributeValue("provider_type", cty.StringVal(notificationProviderToString(n.GetProvider()))) - - if ids := n.GetMonitorIds(); len(ids) > 0 { - vals := make([]cty.Value, len(ids)) - for i, id := range ids { - vals[i] = cty.StringVal(id) - } - b.SetAttributeValue("monitor_ids", cty.SetVal(vals)) - } + b.SetAttributeValue("provider_type", cty.StringVal(providerType)) + g.writeMonitorIds(b, n.GetMonitorIds()) g.writeNotificationProvider(b, n) body.AppendNewline() @@ -244,6 +256,80 @@ func (g *Generator) GenerateNotificationsFile() *hclwrite.File { return f } +func (g *Generator) writeMonitorIds(b *hclwrite.Body, ids []string) { + if len(ids) == 0 { + return + } + sorted := append([]string(nil), ids...) + sort.Strings(sorted) + + tokens := hclwrite.Tokens{ + {Type: hclsyntax.TokenOBrack, Bytes: []byte("[")}, + } + for i, id := range sorted { + if i > 0 { + tokens = append(tokens, + &hclwrite.Token{Type: hclsyntax.TokenComma, Bytes: []byte(",")}, + &hclwrite.Token{Type: hclsyntax.TokenIdent, Bytes: []byte(" ")}, + ) + } + if ref, found := g.monitorRefs[id]; found { + tokens = append(tokens, traversalTokensInline(ref.ResourceType, ref.Name, "id")...) + } else { + tokens = append(tokens, stringLitTokens(id)...) + } + } + tokens = append(tokens, + &hclwrite.Token{Type: hclsyntax.TokenCBrack, Bytes: []byte("]")}, + &hclwrite.Token{Type: hclsyntax.TokenNewline, Bytes: []byte("\n")}, + ) + b.SetAttributeRaw("monitor_ids", tokens) +} + +func stringLitTokens(s string) hclwrite.Tokens { + return hclwrite.Tokens{ + {Type: hclsyntax.TokenOQuote, Bytes: []byte(`"`)}, + {Type: hclsyntax.TokenQuotedLit, Bytes: []byte(s)}, + {Type: hclsyntax.TokenCQuote, Bytes: []byte(`"`)}, + } +} + +func renderableNotification(n *notificationv1.Notification) (string, bool) { + data := n.GetData() + if data == nil { + return "", false + } + switch data.Data.(type) { + case *notificationv1.NotificationData_Discord: + return "discord", true + case *notificationv1.NotificationData_Email: + return "email", true + case *notificationv1.NotificationData_Slack: + return "slack", true + case *notificationv1.NotificationData_Pagerduty: + return "pagerduty", true + case *notificationv1.NotificationData_Opsgenie: + return "opsgenie", true + case *notificationv1.NotificationData_Webhook: + return "webhook", true + case *notificationv1.NotificationData_Telegram: + return "telegram", true + case *notificationv1.NotificationData_Sms: + return "sms", true + case *notificationv1.NotificationData_Whatsapp: + return "whatsapp", true + case *notificationv1.NotificationData_GoogleChat: + return "google_chat", true + case *notificationv1.NotificationData_GrafanaOncall: + return "grafana_oncall", true + case *notificationv1.NotificationData_Ntfy: + return "ntfy", true + case *notificationv1.NotificationData_MsTeams: + return "ms_teams", true + } + return "", false +} + func (g *Generator) writeNotificationProvider(b *hclwrite.Body, n *notificationv1.Notification) { data := n.GetData() if data == nil { @@ -272,10 +358,18 @@ func (g *Generator) writeNotificationProvider(b *hclwrite.Body, n *notificationv case *notificationv1.NotificationData_Webhook: pb := b.AppendNewBlock("webhook", nil).Body() pb.SetAttributeValue("endpoint", cty.StringVal(d.Webhook.GetEndpoint())) + vals := make([]cty.Value, 0, len(d.Webhook.GetHeaders())) for _, h := range d.Webhook.GetHeaders() { - hb := pb.AppendNewBlock("headers", nil).Body() - hb.SetAttributeValue("key", cty.StringVal(h.GetKey())) - hb.SetAttributeValue("value", cty.StringVal(h.GetValue())) + if h.GetKey() == "" { + continue + } + vals = append(vals, cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal(h.GetKey()), + "value": cty.StringVal(h.GetValue()), + })) + } + if len(vals) > 0 { + pb.SetAttributeValue("headers", cty.ListVal(vals)) } case *notificationv1.NotificationData_Telegram: pb := b.AppendNewBlock("telegram", nil).Body() @@ -302,6 +396,9 @@ func (g *Generator) writeNotificationProvider(b *hclwrite.Body, n *notificationv appendTODOComment(pb) pb.SetAttributeValue("token", cty.StringVal("REPLACE_ME")) } + case *notificationv1.NotificationData_MsTeams: + pb := b.AppendNewBlock("ms_teams", nil).Body() + pb.SetAttributeValue("webhook_url", cty.StringVal(d.MsTeams.GetWebhookUrl())) } } @@ -333,14 +430,60 @@ func (g *Generator) GenerateStatusPagesFile() *hclwrite.File { b.SetAttributeValue("custom_domain", cty.StringVal(page.GetCustomDomain())) } - accessType := pageAccessTypeToString(page.GetAccessType()) - if accessType != "public" { - b.SetAttributeValue("access_type", cty.StringVal(accessType)) - if accessType == "password" { + switch pageAccessTypeToString(page.GetAccessType()) { + case "password": + b.SetAttributeValue("access_type", cty.StringVal("password")) + appendTODOComment(b) + b.SetAttributeValue("password", cty.StringVal("REPLACE_ME")) + case "email-domain": + b.SetAttributeValue("access_type", cty.StringVal("email-domain")) + domains := append([]string(nil), page.GetAuthEmailDomains()...) + sort.Strings(domains) + if len(domains) > 0 { + vals := make([]cty.Value, len(domains)) + for i, d := range domains { + vals[i] = cty.StringVal(d) + } + b.SetAttributeValue("auth_email_domains", cty.ListVal(vals)) + } else { appendTODOComment(b) - b.SetAttributeValue("password", cty.StringVal("REPLACE_ME")) + b.SetAttributeValue("auth_email_domains", cty.ListVal([]cty.Value{cty.StringVal("REPLACE_ME")})) + } + case "ip": + b.SetAttributeValue("access_type", cty.StringVal("ip")) + if r := page.GetAllowedIpRanges(); r != "" { + b.SetAttributeValue("allowed_ip_ranges", cty.StringVal(r)) + } else { + appendTODOComment(b) + b.SetAttributeValue("allowed_ip_ranges", cty.StringVal("REPLACE_ME")) + } + } + + if theme := pageThemeToString(page.GetTheme()); theme != "" && theme != "system" { + b.SetAttributeValue("theme", cty.StringVal(theme)) + } + if dl := localeToString(page.GetDefaultLocale()); dl != "" && dl != "en" { + b.SetAttributeValue("default_locale", cty.StringVal(dl)) + } + if locs := page.GetLocales(); len(locs) > 0 { + strs := make([]string, 0, len(locs)) + for _, l := range locs { + if s := localeToString(l); s != "" { + strs = append(strs, s) + } + } + if len(strs) > 0 { + sort.Strings(strs) + vals := make([]cty.Value, len(strs)) + for i, s := range strs { + vals[i] = cty.StringVal(s) + } + b.SetAttributeValue("locales", cty.ListVal(vals)) } } + if page.GetAllowIndex() { + b.SetAttributeValue("allow_index", cty.BoolVal(true)) + } body.AppendNewline() @@ -350,6 +493,9 @@ func (g *Generator) GenerateStatusPagesFile() *hclwrite.File { gb := body.AppendNewBlock("resource", []string{"openstatus_status_page_component_group", gName}).Body() setTraversalOrString(gb, "page_id", g.pageRefs, page.GetId()) gb.SetAttributeValue("name", cty.StringVal(grp.GetName())) + if grp.GetDefaultOpen() { + gb.SetAttributeValue("default_open", cty.BoolVal(true)) + } body.AppendNewline() } @@ -397,6 +543,9 @@ func (g *Generator) GenerateImportsFile() *hclwrite.File { writeImportBlock(body, "openstatus_dns_monitor", g.dnsMonitorNames[m.GetId()], m.GetId()) } for _, n := range g.data.Notifications { + if g.skippedNotifications[n.GetId()] { + continue + } writeImportBlock(body, "openstatus_notification", g.notifNames[n.GetId()], n.GetId()) } for _, sp := range g.data.StatusPages { @@ -439,13 +588,41 @@ func writeRegions(b *hclwrite.Body, regions []monitorv1.Region) { if len(regions) == 0 { return } - vals := make([]cty.Value, len(regions)) + strs := make([]string, len(regions)) for i, r := range regions { - vals[i] = cty.StringVal(regionToTerraform(r)) + strs[i] = regionToTerraform(r) + } + sort.Strings(strs) + vals := make([]cty.Value, len(strs)) + for i, s := range strs { + vals[i] = cty.StringVal(s) } b.SetAttributeValue("regions", cty.ListVal(vals)) } +func writeOpenTelemetry(b *hclwrite.Body, ot *monitorv1.OpenTelemetryConfig) { + if ot == nil { + return + } + endpoint := ot.GetEndpoint() + headers := ot.GetHeaders() + if endpoint == "" && len(headers) == 0 { + return + } + otb := b.AppendNewBlock("open_telemetry", nil).Body() + if endpoint != "" { + otb.SetAttributeValue("endpoint", cty.StringVal(endpoint)) + } + for _, h := range headers { + if h.GetKey() == "" { + continue + } + hb := otb.AppendNewBlock("headers", nil).Body() + hb.SetAttributeValue("key", cty.StringVal(h.GetKey())) + hb.SetAttributeValue("value", cty.StringVal(h.GetValue())) + } +} + func writeHeaders(b *hclwrite.Body, headers []*monitorv1.Headers) { for _, h := range headers { if h.GetKey() == "" { @@ -499,6 +676,15 @@ func setTraversalOrString(b *hclwrite.Body, attr string, refs map[string]resourc } func traversalTokens(parts ...string) hclwrite.Tokens { + tokens := traversalTokensInline(parts...) + tokens = append(tokens, &hclwrite.Token{ + Type: hclsyntax.TokenNewline, + Bytes: []byte("\n"), + }) + return tokens +} + +func traversalTokensInline(parts ...string) hclwrite.Tokens { tokens := hclwrite.Tokens{} for i, part := range parts { if i > 0 { @@ -512,10 +698,6 @@ func traversalTokens(parts ...string) hclwrite.Tokens { Bytes: []byte(part), }) } - tokens = append(tokens, &hclwrite.Token{ - Type: hclsyntax.TokenNewline, - Bytes: []byte("\n"), - }) return tokens } diff --git a/internal/terraform/smoke_test.go b/internal/terraform/smoke_test.go new file mode 100644 index 0000000..9c9d2be --- /dev/null +++ b/internal/terraform/smoke_test.go @@ -0,0 +1,192 @@ +//go:build smoke + +// Run with: go test -tags=smoke ./internal/terraform/ +// +// This smoke test exercises the generator's output against a real terraform +// binary and the live openstatusHQ/openstatus provider from the public +// registry. It does NOT require an API token and does NOT call any +// OpenStatus RPC — only `terraform init` (which fetches the provider) and +// `terraform validate` (which checks schema/syntax fit). +// +// Skipped automatically when the terraform binary is not on PATH. + +package terraform + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + monitorv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/monitor/v1" + notificationv1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/notification/v1" + status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" +) + +func TestSmokeValidate(t *testing.T) { + if _, err := exec.LookPath("terraform"); err != nil { + t.Skipf("terraform binary not in PATH; install terraform 1.0+ to run this test") + } + + dir := t.TempDir() + data := smokeFixture() + gen := NewGenerator(data) + + files := map[string][]byte{ + "provider.tf": GenerateProviderFile(), + "monitors.tf": gen.GenerateMonitorsFile().Bytes(), + "notifications.tf": gen.GenerateNotificationsFile().Bytes(), + "status_pages.tf": gen.GenerateStatusPagesFile().Bytes(), + "imports.tf": gen.GenerateImportsFile().Bytes(), + } + for name, content := range files { + if err := os.WriteFile(filepath.Join(dir, name), content, 0644); err != nil { + t.Fatalf("writing %s: %v", name, err) + } + } + + initCmd := exec.Command("terraform", "init", "-upgrade", "-no-color") + initCmd.Dir = dir + if out, err := initCmd.CombinedOutput(); err != nil { + t.Fatalf("terraform init failed: %v\noutput:\n%s", err, out) + } + + validateCmd := exec.Command("terraform", "validate", "-no-color") + validateCmd.Dir = dir + if out, err := validateCmd.CombinedOutput(); err != nil { + t.Fatalf("terraform validate failed: %v\noutput:\n%s", err, out) + } +} + +func smokeFixture() *WorkspaceData { + httpMon := &monitorv1.HTTPMonitor{} + httpMon.SetId("mon-http") + httpMon.SetName("API Health") + httpMon.SetUrl("https://api.example.com/health") + httpMon.SetPeriodicity(monitorv1.Periodicity_PERIODICITY_5M) + httpMon.SetMethod(monitorv1.HTTPMethod_HTTP_METHOD_GET) + httpMon.SetTimeout(45000) + httpMon.SetRetry(3) + httpMon.SetFollowRedirects(true) + httpMon.SetActive(true) + httpMon.SetRegions([]monitorv1.Region{monitorv1.Region_REGION_FLY_IAD}) + + otelConfig := &monitorv1.OpenTelemetryConfig{} + otelConfig.SetEndpoint("https://otel.example.com/v1/metrics") + otelHeader := &monitorv1.Headers{} + otelHeader.SetKey("X-Api-Key") + otelHeader.SetValue("secret") + otelConfig.SetHeaders([]*monitorv1.Headers{otelHeader}) + httpMon.SetOpenTelemetry(otelConfig) + + statusAssertion := &monitorv1.StatusCodeAssertion{} + statusAssertion.SetTarget(200) + statusAssertion.SetComparator(monitorv1.NumberComparator_NUMBER_COMPARATOR_EQUAL) + httpMon.SetStatusCodeAssertions([]*monitorv1.StatusCodeAssertion{statusAssertion}) + + tcpMon := &monitorv1.TCPMonitor{} + tcpMon.SetId("mon-tcp") + tcpMon.SetName("DB TCP") + tcpMon.SetUri("db.example.com:5432") + tcpMon.SetPeriodicity(monitorv1.Periodicity_PERIODICITY_5M) + tcpMon.SetTimeout(45000) + tcpMon.SetRetry(3) + tcpMon.SetActive(true) + + dnsMon := &monitorv1.DNSMonitor{} + dnsMon.SetId("mon-dns") + dnsMon.SetName("DNS Check") + dnsMon.SetUri("example.com") + dnsMon.SetPeriodicity(monitorv1.Periodicity_PERIODICITY_10M) + dnsMon.SetTimeout(45000) + dnsMon.SetRetry(3) + dnsMon.SetActive(true) + dnsRecord := &monitorv1.RecordAssertion{} + dnsRecord.SetRecord("A") + dnsRecord.SetTarget("93.184.216.34") + dnsRecord.SetComparator(monitorv1.RecordComparator_RECORD_COMPARATOR_EQUAL) + dnsMon.SetRecordAssertions([]*monitorv1.RecordAssertion{dnsRecord}) + + slackNotif := newNotification("notif-slack", "Slack Alerts", []string{"mon-http"}, func(d *notificationv1.NotificationData) { + sd := ¬ificationv1.SlackData{} + sd.SetWebhookUrl("https://hooks.example.com/slack") + d.SetSlack(sd) + }) + teamsNotif := newNotification("notif-teams", "Teams Alerts", nil, func(d *notificationv1.NotificationData) { + td := ¬ificationv1.MsTeamsData{} + td.SetWebhookUrl("https://example.com/teams") + d.SetMsTeams(td) + }) + webhookNotif := newNotification("notif-webhook", "Webhook Alerts", nil, func(d *notificationv1.NotificationData) { + wd := ¬ificationv1.WebhookData{} + wd.SetEndpoint("https://example.com/hook") + h := ¬ificationv1.WebhookHeader{} + h.SetKey("Authorization") + h.SetValue("Bearer xyz") + wd.SetHeaders([]*notificationv1.WebhookHeader{h}) + d.SetWebhook(wd) + }) + + page := &status_pagev1.StatusPage{} + page.SetId("page-1") + page.SetTitle("Public Status") + page.SetSlug("public-status") + page.SetTheme(status_pagev1.PageTheme_PAGE_THEME_DARK) + page.SetDefaultLocale(status_pagev1.Locale_LOCALE_EN) + page.SetLocales([]status_pagev1.Locale{ + status_pagev1.Locale_LOCALE_EN, + status_pagev1.Locale_LOCALE_FR, + }) + page.SetAllowIndex(true) + + group := &status_pagev1.PageComponentGroup{} + group.SetId("group-1") + group.SetPageId("page-1") + group.SetName("Infrastructure") + group.SetDefaultOpen(true) + + component := &status_pagev1.PageComponent{} + component.SetId("comp-1") + component.SetPageId("page-1") + component.SetName("API") + component.SetType(status_pagev1.PageComponentType_PAGE_COMPONENT_TYPE_MONITOR) + component.SetMonitorId("mon-http") + component.SetOrder(1) + component.SetGroupId("group-1") + component.SetGroupOrder(1) + + ipPage := &status_pagev1.StatusPage{} + ipPage.SetId("page-2") + ipPage.SetTitle("Internal") + ipPage.SetSlug("internal") + ipPage.SetAccessType(status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_IP_RESTRICTED) + ipPage.SetAllowedIpRanges("10.0.0.0/8,192.168.0.0/16") + + return &WorkspaceData{ + HTTPMonitors: []*monitorv1.HTTPMonitor{httpMon}, + TCPMonitors: []*monitorv1.TCPMonitor{tcpMon}, + DNSMonitors: []*monitorv1.DNSMonitor{dnsMon}, + Notifications: []*notificationv1.Notification{slackNotif, teamsNotif, webhookNotif}, + StatusPages: []StatusPageData{ + { + Page: page, + Groups: []*status_pagev1.PageComponentGroup{group}, + Components: []*status_pagev1.PageComponent{component}, + }, + {Page: ipPage}, + }, + } +} + +func newNotification(id, name string, monitorIDs []string, setData func(*notificationv1.NotificationData)) *notificationv1.Notification { + n := ¬ificationv1.Notification{} + n.SetId(id) + n.SetName(name) + if len(monitorIDs) > 0 { + n.SetMonitorIds(monitorIDs) + } + nd := ¬ificationv1.NotificationData{} + setData(nd) + n.SetData(nd) + return n +} diff --git a/internal/wizard/statuspage.go b/internal/wizard/statuspage.go index 26bc70f..2b6cecf 100644 --- a/internal/wizard/statuspage.go +++ b/internal/wizard/statuspage.go @@ -3,8 +3,8 @@ package wizard import ( "context" - status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "buf.build/gen/go/openstatus/api/connectrpc/gosimple/openstatus/status_page/v1/status_pagev1connect" + status_pagev1 "buf.build/gen/go/openstatus/api/protocolbuffers/go/openstatus/status_page/v1" "connectrpc.com/connect" "github.com/openstatusHQ/cli/internal/api" output "github.com/openstatusHQ/cli/internal/cli" diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..2305074 --- /dev/null +++ b/plan.md @@ -0,0 +1,548 @@ +# Sync plan: `openstatus terraform generate` ↔ terraform-provider-openstatus + +**Goal.** Bring the HCL produced by `openstatus terraform generate` into one-to-one alignment with the schema of `terraform-provider-openstatus@v0.2.0`. Every workspace resource must round-trip through `terraform import → terraform plan` with **no drift** and **no validation errors**. + +**Inputs.** +- Local: `internal/terraform/` (`generate.go`, `fetch.go`, `hcl.go`, `enums.go`, `regions.go`, `naming.go`, `generate_test.go`). +- Provider: `github.com/openstatusHQ/terraform-provider-openstatus` @ `main` (v0.2.0). +- Proto API: `github.com/openstatusHQ/openstatus/packages/proto/api/openstatus/v1` (monitor, notification, status_page, plus unused maintenance / status_report). +- Pinned SDK after `go get -u`: `buf.build/gen/go/openstatus/api/...@v1.36.11-20260512200453-7d7b7047611f.1`. Every symbol referenced below is verified present in this pin. + +**Out of scope.** Provider-side changes; new RPCs; non-export commands. The generator is read-only: it consumes List+Get RPCs and writes HCL. + +--- + +## 1. What syncs and what doesn't + +| Provider resource (v0.2.0) | Provider attrs / blocks | Generator today | Action | +|---|---|---|---| +| `openstatus_http_monitor` | name, url, periodicity, method, body, timeout, degraded_at, retry, follow_redirects, active, public, description, regions; blocks: headers, status_code_assertions, body_assertions, header_assertions, **open_telemetry** | All scalar fields ✓; all blocks except `open_telemetry` ✓ | §3.1 add `open_telemetry` | +| `openstatus_tcp_monitor` | name, uri, periodicity, timeout, degraded_at, retry, active, public, description, regions; **open_telemetry** | Scalars ✓; no blocks | §3.1 add `open_telemetry` | +| `openstatus_dns_monitor` | …+ record_assertions, **open_telemetry** | Scalars + record_assertions ✓ | §3.1 add `open_telemetry` | +| `openstatus_notification` | name, provider_type, monitor_ids; 13 inner blocks incl. **ms_teams** | 12 inner blocks; `ms_teams` missing | §3.2 | +| `openstatus_status_page` | title, slug, description, homepage_url, contact_url, icon, custom_domain, access_type, password, **auth_email_domains, allowed_ip_ranges, theme, default_locale, locales, allow_index** | Only the first 8 + conditional password | §3.3 | +| `openstatus_status_page_component_group` | page_id, name, **default_open** | page_id, name only | §3.4 | +| `openstatus_status_page_component` | page_id, type, monitor_id, name, description, order, group_id, group_order | Complete ✓ | none | + +**No provider resource exists** for maintenances or status reports. The generator ignores them entirely — no fetch, no summary, no sidecar files. Document upstream if/when the provider gains them. + +**Provider version constraint** in generated `provider.tf` is still `~> 0.1.0`; the v0.2.0 schema additions require `~> 0.2`. Fix in §3.0. + +--- + +## 2. Strings to keep verbatim + +These are the exact tf-string values the provider's `OneOf` validators accept (cross-checked against `internal/monitor/common.go` and `internal/statuspage/*` in the provider repo). The generator's existing `enums.go`/`regions.go` helpers already emit these correctly except where flagged: + +- `periodicity`: `30s`, `1m`, `5m`, `10m`, `30m`, `1h` +- `method`: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`, `TRACE`, `CONNECT` +- regions: 28 values — `fly-{ams,arn,bom,cdg,dfw,ewr,fra,gru,iad,jnb,lax,lhr,nrt,ord,sjc,sin,syd,yyz}`, `koyeb-{fra,par,sfo,sin,tyo,was}`, `railway-{us-west2,us-east4,europe-west4,asia-southeast1}` +- number comparator: `eq`, `neq`, `gt`, `gte`, `lt`, `lte` +- string comparator: + `contains`, `not_contains`, `empty`, `not_empty` +- record comparator: `eq`, `neq`, `contains`, `not_contains` +- DNS record: `A`, `AAAA`, `CNAME`, `MX`, `TXT` +- notification `provider_type`: `discord`, `email`, `slack`, `pagerduty`, `opsgenie`, `webhook`, `telegram`, `sms`, `whatsapp`, `google_chat`, `grafana_oncall`, `ntfy`, **`ms_teams`** (missing today) +- opsgenie region: `us`, `eu` +- page component type: `monitor`, `static` +- status page `access_type`: `public`, `password`, `email-domain`, **`ip`** (missing today) +- status page `theme`: `system`, `light`, `dark` +- locale: `en`, `fr`, `de` + +--- + +## 2b. Determinism rules + +The generator must produce byte-identical output when re-run against an unchanged workspace, so re-export diffs stay readable. Rule: + +- **Sort** (sets — order is irrelevant to the provider): `monitor_ids`, `regions`. +- **Sort** (lists where order is presentational only): `locales`, `auth_email_domains`. +- **Preserve API order** (lists where order may be meaningful to the user): assertion lists (`status_code_assertions`, `body_assertions`, `header_assertions`, `record_assertions`), monitor `headers`, webhook headers, OTEL headers. +- **Preserve API order** for top-level resources (monitors, notifications, pages) — the API returns them in roughly `created_at` order, which is stable enough. + +Apply via `sort.Strings(...)` before each affected emission. Helper not needed; ~5 LOC total. + +--- + +## 3. Sync checklist + +Each item is independent and can ship as a separate PR. Suggested order is roughly safety-first (correctness bugs before drift fixes). + +### 3.0 Bump provider version constraint in generated `provider.tf` + +`internal/terraform/hcl.go:91-103` — `GenerateProviderFile`. + +```diff +- version = "~> 0.1.0" ++ version = "~> 0.2" +``` + +Test: update `TestGenerateProviderFile` in `generate_test.go:13-18`. + +Pair with §3.6 init-upgrade hint so users on a previously-generated workspace know to run `terraform init -upgrade` after re-running the command. + +--- + +### 3.1 Monitors: emit `open_telemetry` block on HTTP, TCP, DNS + +Three monitor builders in `internal/terraform/hcl.go` lines 109, 151, 180. Add a helper alongside `writeRegions`: + +```go +// hcl.go — new helper, place near writeHeaders +func writeOpenTelemetry(b *hclwrite.Body, ot *monitorv1.OpenTelemetryConfig) { + if ot == nil { + return + } + endpoint := ot.GetEndpoint() + headers := ot.GetHeaders() + if endpoint == "" && len(headers) == 0 { + return + } + otb := b.AppendNewBlock("open_telemetry", nil).Body() + if endpoint != "" { + otb.SetAttributeValue("endpoint", cty.StringVal(endpoint)) + } + for _, h := range headers { + if h.GetKey() == "" { + continue + } + hb := otb.AppendNewBlock("headers", nil).Body() + hb.SetAttributeValue("key", cty.StringVal(h.GetKey())) + hb.SetAttributeValue("value", cty.StringVal(h.GetValue())) + } +} +``` + +Call it from each monitor branch, after the assertion writers (HTTP) or after `writeRegions` (TCP/DNS). + +SDK getters used: `*HTTPMonitor.GetOpenTelemetry()`, `*TCPMonitor.GetOpenTelemetry()`, `*DNSMonitor.GetOpenTelemetry()` — confirmed present in pinned SDK. + +Tests: extend `TestGenerateMonitorsFile_HTTP` (and add TCP/DNS variants) to assert that an HTTP monitor with `OpenTelemetryConfig{Endpoint:"https://otel.example.com/v1/metrics", Headers:[{X-Api-Key,secret}]}` produces the block shown in `examples/resources/openstatus_http_monitor/resource.tf`. + +--- + +### 3.2 Notifications: refactor + correctness fixes + +A single block of work covering four related changes. All edits in `internal/terraform/hcl.go` (`writeNotificationProvider` and `GenerateNotificationsFile`) and `internal/terraform/enums.go` (`notificationProviderToString`). + +**A. Single source of truth driven by the data oneof.** Replace the two-source approach (`notificationProviderToString(n.GetProvider())` for the `provider_type` attribute plus a parallel `switch d := data.Data` for the block) with a single switch on `data.Data` that yields both the provider string and the emitted block. Prevents server-side mismatches between `provider` and `data` from producing broken HCL. + +```go +func writeNotificationProvider(b *hclwrite.Body, n *notificationv1.Notification) (providerType string, ok bool) { + data := n.GetData() + if data == nil { + return "", false + } + switch d := data.Data.(type) { + case *notificationv1.NotificationData_Discord: + pb := b.AppendNewBlock("discord", nil).Body() + pb.SetAttributeValue("webhook_url", cty.StringVal(d.Discord.GetWebhookUrl())) + return "discord", true + // …one case per provider type, including ms_teams (new)… + case *notificationv1.NotificationData_MsTeams: + pb := b.AppendNewBlock("ms_teams", nil).Body() + pb.SetAttributeValue("webhook_url", cty.StringVal(d.MsTeams.GetWebhookUrl())) + return "ms_teams", true + } + return "", false +} +``` + +Caller (`GenerateNotificationsFile`) inverts to "block first, then attributes": peek the data oneof to decide whether to emit at all, write the resource header, then call `writeNotificationProvider` which returns the inferred `provider_type` and emits the inner block in one pass. + +**B. Add `ms_teams`** — covered by (A). Pinned SDK confirmed to include `NotificationProvider_NOTIFICATION_PROVIDER_MS_TEAMS = 13`, `MsTeamsData{WebhookUrl}`, and the `NotificationData_MsTeams` oneof case. + +**C. Fix webhook headers — `ListNestedAttribute`, not block.** The provider's webhook schema declares `headers` as `schema.ListNestedAttribute`. The current generator emits `headers { key=… value=… }` block syntax which the provider rejects. Replace with list-attribute syntax: + +```go +case *notificationv1.NotificationData_Webhook: + pb := b.AppendNewBlock("webhook", nil).Body() + pb.SetAttributeValue("endpoint", cty.StringVal(d.Webhook.GetEndpoint())) + if hs := d.Webhook.GetHeaders(); len(hs) > 0 { + vals := make([]cty.Value, 0, len(hs)) + for _, h := range hs { + if h.GetKey() == "" { continue } + vals = append(vals, cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal(h.GetKey()), + "value": cty.StringVal(h.GetValue()), + })) + } + if len(vals) > 0 { + pb.SetAttributeValue("headers", cty.ListVal(vals)) + } + } + return "webhook", true +``` + +**D. Emit `monitor_ids` as traversals when the id is in `monitorRefs`; fall back to plain string when it isn't.** Matches the pattern `setTraversalOrString` already uses for singular cross-refs. The string fallback intentionally preserves the id in HCL even when the monitor isn't in the workspace (race between `ListMonitors` and `ListNotifications`, or monitor deleted out-of-band) — terraform will surface the inconsistency at plan time rather than the generator silently dropping it. + +Build the set manually using hclwrite tokens so traversals and string literals can coexist in one set. Skeleton: + +```go +if ids := n.GetMonitorIds(); len(ids) > 0 { + tokens := hclwrite.Tokens{ /* '[' */ } + for i, id := range ids { + if i > 0 { tokens = append(tokens, commaToken) } + if ref, found := g.monitorRefs[id]; found { + tokens = append(tokens, identTraversal(ref.ResourceType, ref.Name, "id")...) + } else { + tokens = append(tokens, stringLiteral(id)...) + } + } + tokens = append(tokens, /* ']' */) + b.SetAttributeRaw("monitor_ids", tokens) +} +``` + +**E. Skip + warn on unknown / UNSPECIFIED providers.** `writeNotificationProvider` returning `ok=false` triggers the caller to: +- Print `warning: skipping notification %q — unknown provider type (CLI may be outdated)` to stderr. +- Continue past this notification without writing a resource block. +- Exclude the notification's id from `GenerateImportsFile`. + +Tests: +- `TestGenerateNotificationsFile_MsTeams` — provider type, `ms_teams { webhook_url }` block. +- `TestGenerateNotificationsFile_WebhookHeaders` — assert `headers = [{key = "X", value = "Y"}]` attribute syntax, not block. +- `TestGenerateNotificationsFile_MonitorIdsTraversal` — workspace with one matching monitor and one unknown id → list has `openstatus_http_monitor.foo.id` and `"unknown-id"` mixed. +- `TestGenerateNotificationsFile_UnknownProviderSkipped` — UNSPECIFIED provider → no resource block in output, no import in imports.tf. + +--- + +### 3.3 Status pages: correctness pack + +Two correctness bugs and four drift gaps in one resource. All edits land in `internal/terraform/hcl.go:308-345` (`GenerateStatusPagesFile`) and `internal/terraform/enums.go:167-178` (`pageAccessTypeToString`). + +**A. Fix `access_type = "ip"` being silently dropped.** Today the `pageAccessTypeToString` switch has no `IP_RESTRICTED` case and falls through to `"public"`, which (a) loses the user's choice and (b) drops the required `allowed_ip_ranges`. Add: + +```go +case status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_IP_RESTRICTED: + return "ip" +``` + +**B. Emit `auth_email_domains` and `allowed_ip_ranges`.** These are required by the provider's `ValidateConfig` when `access_type` is `email-domain` / `ip`. Without them, the generated HCL **fails plan**. Replace the `access_type != "public"` block in `GenerateStatusPagesFile` (around hcl.go:336-343) with: + +```go +switch accessType { +case "password": + b.SetAttributeValue("access_type", cty.StringVal("password")) + appendTODOComment(b) + b.SetAttributeValue("password", cty.StringVal("REPLACE_ME")) +case "email-domain": + b.SetAttributeValue("access_type", cty.StringVal("email-domain")) + domains := page.GetAuthEmailDomains() + vals := make([]cty.Value, len(domains)) + for i, d := range domains { + vals[i] = cty.StringVal(d) + } + b.SetAttributeValue("auth_email_domains", cty.ListVal(vals)) // safe: provider requires ≥1 +case "ip": + b.SetAttributeValue("access_type", cty.StringVal("ip")) + b.SetAttributeValue("allowed_ip_ranges", cty.StringVal(page.GetAllowedIpRanges())) +} +``` + +(`access_type = "public"` continues to be omitted as a default.) + +**C. Emit `theme`, `default_locale`, `locales`, `allow_index`** with skip-default rules to avoid drift on the next plan: + +```go +if theme := pageThemeToString(page.GetTheme()); theme != "" && theme != "system" { + b.SetAttributeValue("theme", cty.StringVal(theme)) +} +if dl := localeToString(page.GetDefaultLocale()); dl != "" && dl != "en" { + b.SetAttributeValue("default_locale", cty.StringVal(dl)) +} +if locs := page.GetLocales(); len(locs) > 0 { + vals := make([]cty.Value, 0, len(locs)) + for _, l := range locs { + if s := localeToString(l); s != "" { + vals = append(vals, cty.StringVal(s)) + } + } + if len(vals) > 0 { + b.SetAttributeValue("locales", cty.ListVal(vals)) + } +} +if page.GetAllowIndex() { + b.SetAttributeValue("allow_index", cty.BoolVal(true)) +} +``` + +Helpers to add in `enums.go`: + +```go +func pageThemeToString(t status_pagev1.PageTheme) string { + switch t { + case status_pagev1.PageTheme_PAGE_THEME_SYSTEM: + return "system" + case status_pagev1.PageTheme_PAGE_THEME_LIGHT: + return "light" + case status_pagev1.PageTheme_PAGE_THEME_DARK: + return "dark" + } + return "" +} + +func localeToString(l status_pagev1.Locale) string { + switch l { + case status_pagev1.Locale_LOCALE_EN: + return "en" + case status_pagev1.Locale_LOCALE_FR: + return "fr" + case status_pagev1.Locale_LOCALE_DE: + return "de" + } + return "" +} +``` + +SDK confirmed: `PageTheme = {UNSPECIFIED, SYSTEM, LIGHT, DARK}`, `Locale = {UNSPECIFIED, EN, FR, DE}`, `PageAccessType.IP_RESTRICTED = 4`, and `StatusPage.GetTheme/DefaultLocale/Locales/AllowIndex/AuthEmailDomains/AllowedIpRanges` all present. + +Tests: add cases to `generate_test.go`: +- `TestGenerateStatusPagesFile_IPAccess` — IP_RESTRICTED → `access_type = "ip"` + `allowed_ip_ranges`. +- `TestGenerateStatusPagesFile_EmailDomainAccess` — AUTHENTICATED → `access_type = "email-domain"` + `auth_email_domains = [...]`. +- `TestGenerateStatusPagesFile_ThemeLocaleAllowIndex` — dark/fr-locale page emits the three attrs; default page emits none. + +--- + +### 3.4 Component groups: emit `default_open` + +`internal/terraform/hcl.go:348-354`. Skip-default rule (provider default is `false`): + +```go +if grp.GetDefaultOpen() { + gb.SetAttributeValue("default_open", cty.BoolVal(true)) +} +``` + +SDK confirmed: `PageComponentGroup.GetDefaultOpen() bool`. + +Test: extend `TestGenerateStatusPagesFile` to include a group with `default_open=true` and assert the line is emitted. + +--- + +### 3.5 CLI ergonomics: `--force` and init-upgrade hint + +`internal/terraform/generate.go`. + +**A. `--force` flag, refuse-by-default overwrites.** Today `writeFile` truncates blindly. Add a stat-and-bail step before any write: if any of `provider.tf`, `monitors.tf`, `notifications.tf`, `status_pages.tf`, `imports.tf` already exists in `--output-dir` and `--force` is not set, exit with: + +``` +error: refusing to overwrite existing file %s; pass --force to replace +``` + +Sketch: + +```go +&cli.BoolFlag{ + Name: "force", + Usage: "Overwrite existing files in --output-dir", + Aliases: []string{"f"}, +}, +// ... +if !cmd.Bool("force") { + for _, name := range []string{"provider.tf", "monitors.tf", "notifications.tf", "status_pages.tf", "imports.tf"} { + if _, err := os.Stat(filepath.Join(outputDir, name)); err == nil { + return cli.Exit(fmt.Sprintf("refusing to overwrite existing file %s; pass --force to replace", name), 1) + } + } +} +``` + +The check happens after the API fetch fails-fast but before any disk writes, so partial output is impossible. + +**B. Init-upgrade hint.** Always append to `printSummary` (`generate.go:130`): + +``` +Note: provider version pinned to ~> 0.2. Run 'terraform init -upgrade' if you previously ran this command. +``` + +Tests: extend `generate_test.go` (or add a small `cli_test.go`) covering: +- Refusal when a target file exists and `--force` is unset. +- Overwrite when `--force` is passed. +- Init-upgrade hint present in `printSummary` output. + +--- + +## 4. Test additions checklist + +Append to `internal/terraform/generate_test.go`: + +- [ ] Update `TestGenerateProviderFile` for `~> 0.2`. +- [ ] HTTP monitor `open_telemetry` block. +- [ ] TCP monitor `open_telemetry` block (also adds first TCP test). +- [ ] DNS monitor `open_telemetry` block (existing DNS test extends). +- [ ] Notification `ms_teams` block. +- [ ] Status page IP access (`access_type = "ip"` + `allowed_ip_ranges`). +- [ ] Status page email-domain access (`access_type = "email-domain"` + `auth_email_domains`). +- [ ] Status page theme/default_locale/locales/allow_index emission. +- [ ] Component group `default_open = true`. +- [ ] Notification refactor: provider/data oneof drives both attr and block (no mismatch path). +- [ ] Webhook headers emitted as `headers = [{...}]` attribute (not block). +- [ ] `monitor_ids` emit as traversals for known IDs, plain strings for unknown. +- [ ] UNSPECIFIED / unknown notification provider → resource skipped, no import block, stderr warning. +- [ ] `--force` refuses to overwrite by default; allows overwrite when set. +- [ ] `printSummary` includes the `terraform init -upgrade` hint. + +--- + +## 5. Reference: provider import IDs + +Confirmed against provider `internal/.../ImportState` parsers: + +| Resource | Import ID format | Generator today | +|---|---|---| +| `openstatus_http_monitor` / `_tcp_monitor` / `_dns_monitor` | `` | ✓ | +| `openstatus_notification` | `` | ✓ | +| `openstatus_status_page` | `` | ✓ | +| `openstatus_status_page_component` | `/` | ✓ | +| `openstatus_status_page_component_group` | `/` | ✓ | + +No changes needed in `GenerateImportsFile` (`hcl.go:386-414`). + +--- + +## 6. File-level summary of edits + +| File | Edit | +|---|---| +| `internal/terraform/hcl.go` | `GenerateProviderFile` (version bump); HTTP/TCP/DNS monitor branches (add `writeOpenTelemetry`); `writeNotificationProvider` (add `MsTeams` case); `GenerateStatusPagesFile` (rewrite access-type branch; add theme/locale/allow_index emission); component group branch (`default_open`); new `writeOpenTelemetry` helper | +| `internal/terraform/enums.go` | `notificationProviderToString` (add `MS_TEAMS`); `pageAccessTypeToString` (add `IP_RESTRICTED`); add `pageThemeToString`, `localeToString`. `notificationProviderToString` may move/become driven by `NotificationData` oneof per Q5b. | +| `internal/terraform/generate.go` | `--force` flag + pre-write existence check; init-upgrade hint in `printSummary` | +| `internal/terraform/generate_test.go` | New cases per §4 | +| `internal/cmd/app.go` | Bump `Version` to `"v1.1.0"` | +| `docs/openstatus-docs.md`, `docs/openstatus.1` | Regenerated (see §7) | + +No new files **except** the opt-in smoke test (§9, phase 7). No SDK pin bumps required beyond the dep refresh that has already shipped (`buf.build/gen/go/openstatus/api/...@v1.36.11-20260512200453-7d7b7047611f.1`). + +--- + +## 7. Docs regeneration (last commit of the PR) + +After all code is in and tests pass, regenerate the auto-generated docs from the urfave/cli command tree (the new `--force` flag changes the rendered help text): + +```sh +go run cmd/docs/docs.go +cd docs && pandoc -s -t man openstatus-docs.md -o openstatus.1 +``` + +Commit both `docs/openstatus-docs.md` and `docs/openstatus.1` as the final commit. README is left alone — the team hasn't documented individual `terraform generate` flags in it so far. + +--- + +## 8. Unrelated note from the dep refresh + +`github.com/urfave/cli/v3` moved from `v3.0.0-alpha9.2` → `v3.9.0`, changing `BeforeFunc` to `func(context.Context, *Command) (context.Context, error)`. Already patched at `internal/cmd/app.go:63` — build and tests green. Mentioned here only so reviewers don't wonder why that file changed in the same branch. + +--- + +## 9. Implementation todo list + +Each phase is one commit. Phases are ordered so that earlier work doesn't conflict with later work, and so the build stays green commit-by-commit (a reviewer can `git bisect` cleanly). Run `go build ./... && go test ./...` at the end of every phase before committing. + +### Phase 0 — Pre-flight ✅ + +- [x] Cut a feature branch off `main` (e.g. `feat/tf-generate-sync-v0.2`). _(jj: working on an anonymous change off main; chore: refresh deps committed)_ +- [x] Confirm working tree is clean (`git status`); dep refresh already landed in a previous commit. +- [x] `go build ./...` and `go test ./...` green from baseline. + +### Phase 1 — Bump generated provider version (commit 1: `chore(terraform): pin generated provider to ~> 0.2`) ✅ + +- [x] `internal/terraform/hcl.go` — `GenerateProviderFile`: change `version = "~> 0.1.0"` → `version = "~> 0.2"`. +- [x] `internal/terraform/generate_test.go` — update `TestGenerateProviderFile` assertion to `~> 0.2`. +- [x] `go test ./internal/terraform/...` green. + +### Phase 2 — Status page correctness pack (commit 2: `fix(terraform): emit access_type=ip + auth_email_domains/allowed_ip_ranges; add theme/locale/allow_index`) ✅ + +Order matters within this phase: add helpers first, then call them. + +- [x] `internal/terraform/enums.go` — extend `pageAccessTypeToString` with `case status_pagev1.PageAccessType_PAGE_ACCESS_TYPE_IP_RESTRICTED: return "ip"`. +- [x] `internal/terraform/enums.go` — add `pageThemeToString(t status_pagev1.PageTheme) string` (returns `""` for UNSPECIFIED, `"system"|"light"|"dark"` otherwise). +- [x] `internal/terraform/enums.go` — add `localeToString(l status_pagev1.Locale) string` (returns `""` for UNSPECIFIED, `"en"|"fr"|"de"` otherwise). +- [x] `internal/terraform/hcl.go` — `GenerateStatusPagesFile`: replace the current `accessType != "public"` block with the four-case switch (`public` omits / `password` keeps current TODO+REPLACE_ME / `email-domain` emits `auth_email_domains` sorted via `sort.Strings` or TODO+REPLACE_ME / `ip` emits `allowed_ip_ranges` or TODO+REPLACE_ME). +- [x] `internal/terraform/hcl.go` — `GenerateStatusPagesFile`: after the access-type block, emit `theme` (when not `system`), `default_locale` (when not `en`), `locales` (sorted, when non-empty), `allow_index` (when `true`), using skip-default rules from Q1. +- [x] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_IPAccess` with non-empty `AllowedIpRanges`. +- [x] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_IPAccessEmptyFallback`: IP_RESTRICTED + empty `allowed_ip_ranges` → asserts `# TODO:` comment and `REPLACE_ME` value. +- [x] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_EmailDomainAccess` with non-empty domains. +- [x] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_EmailDomainEmptyFallback`. +- [x] `internal/terraform/generate_test.go` — `TestGenerateStatusPagesFile_ThemeLocaleAllowIndex`: dark + fr default_locale + locales=[en,fr] + allow_index=true → all four emitted; default page → none emitted. +- [x] `go test ./internal/terraform/...` green. + +### Phase 3 — Component group `default_open` (commit 3: `feat(terraform): emit default_open on status page component groups`) ✅ + +- [x] `internal/terraform/hcl.go` — component-group branch in `GenerateStatusPagesFile`: `if grp.GetDefaultOpen() { gb.SetAttributeValue("default_open", cty.BoolVal(true)) }`. +- [x] `internal/terraform/generate_test.go` — extend `TestGenerateStatusPagesFile` (or add a focused test) to include a group with `DefaultOpen: true` and assert the line. +- [x] `go test ./internal/terraform/...` green. + +### Phase 4 — Monitor `open_telemetry` (commit 4: `feat(terraform): emit open_telemetry block on HTTP/TCP/DNS monitors`) ✅ + +- [x] `internal/terraform/hcl.go` — modify the existing `writeRegions` helper to sort regions alphabetically (per §2b — set semantics, deterministic output). +- [x] `internal/terraform/hcl.go` — new helper `writeOpenTelemetry(b *hclwrite.Body, ot *monitorv1.OpenTelemetryConfig)` per Q2: skip iff `ot == nil` OR (`endpoint == "" && len(headers) == 0`); inside, emit `endpoint` only when non-empty; emit one `headers { key/value }` block per header (no sort — preserves API order per §2b). +- [x] Call `writeOpenTelemetry(b, m.GetOpenTelemetry())` in each of: HTTP monitor branch (`hcl.go:109` block, after the assertion writers), TCP monitor branch (`hcl.go:151`, after `writeRegions`), DNS monitor branch (`hcl.go:180`, after `record_assertions`). +- [x] `internal/terraform/generate_test.go` — `TestGenerateMonitorsFile_HTTP_OpenTelemetry`: endpoint + one header → block present. +- [x] `internal/terraform/generate_test.go` — `TestGenerateMonitorsFile_TCP_OpenTelemetry` (also adds first TCP-only test). +- [x] `internal/terraform/generate_test.go` — `TestGenerateMonitorsFile_DNS_OpenTelemetry`. +- [x] `internal/terraform/generate_test.go` — `TestGenerateMonitorsFile_OpenTelemetry_SkippedWhenEmpty`: `OpenTelemetryConfig{Endpoint:"", Headers:nil}` → no block. +- [x] `go test ./internal/terraform/...` green. + +### Phase 5 — Notification refactor + fixes (commit 5: `fix(terraform): notification provider type from data oneof; ms_teams; webhook headers attribute; monitor_ids traversals`) ✅ + +This is the largest phase. Land in one commit (per Q7) but write it incrementally. + +- [x] `internal/terraform/hcl.go` — add the `*notificationv1.NotificationData_MsTeams` case emitting `ms_teams { webhook_url = … }`. +- [x] `internal/terraform/hcl.go` — rewrite the `*notificationv1.NotificationData_Webhook` case so `headers` is emitted as a `cty.ListVal([]cty.Value{cty.ObjectVal({key,value})})` set via `SetAttributeValue("headers", …)`, not as nested `headers { … }` blocks. +- [x] `internal/terraform/hcl.go` — replace the current `monitor_ids` plain-string emission with a token-list builder (`writeMonitorIds`). Sort via `sort.Strings` first; traversal tokens for known refs, string-literal tokens otherwise. +- [x] `internal/terraform/hcl.go` — add `traversalTokensInline(parts ...string)` (no trailing newline) and refactor `traversalTokens` to call it + append newline. Add `stringLitTokens(s)` helper. +- [x] `internal/terraform/hcl.go` — add `renderableNotification(n) (providerType string, ok bool)` switch returning the tf-string per oneof case. +- [x] `internal/terraform/hcl.go` — `Generator` struct: add `skippedNotifications map[string]bool` field; initialize in `NewGenerator`. +- [x] `internal/terraform/hcl.go` — `NewGenerator` notifications loop: skip + warn on `!ok` via `renderableNotification`. +- [x] `internal/terraform/hcl.go` — `GenerateNotificationsFile`: skip when `g.skippedNotifications[n.GetId()]`; provider_type comes from `renderableNotification`. +- [x] `internal/terraform/hcl.go` — `GenerateImportsFile`: same skip guard for the notification import block. +- [x] `internal/terraform/enums.go` — remove `notificationProviderToString` (now unused). +- [x] `internal/terraform/generate_test.go` — `TestGenerateNotificationsFile_MsTeams`. +- [x] `internal/terraform/generate_test.go` — `TestGenerateNotificationsFile_WebhookHeaders`. +- [x] `internal/terraform/generate_test.go` — `TestGenerateNotificationsFile_MonitorIdsTraversal`. +- [x] `internal/terraform/generate_test.go` — `TestGenerateNotificationsFile_UnknownProviderSkipped`. +- [x] `go test ./internal/terraform/...` green. + +### Phase 6 — CLI ergonomics (commit 6: `feat(terraform): --force flag and terraform init -upgrade hint`) ✅ + +- [x] `internal/terraform/generate.go` — add `&cli.BoolFlag{Name: "force", Aliases: []string{"f"}, Usage: "Overwrite existing files in --output-dir"}` to `GetTerraformGenerateCmd().Flags`. +- [x] `internal/terraform/generate.go` — extract `checkExistingFiles(outputDir, force)` helper; invoke before `MkdirAll`. Stats each of `provider.tf`, `monitors.tf`, `notifications.tf`, `status_pages.tf`, `imports.tf` and returns an error mentioning the filename if any exists. +- [x] `internal/terraform/generate.go` — extend `printSummary` with the `terraform init -upgrade` hint. +- [x] `internal/terraform/cli_test.go` — `TestCheckExistingFiles_RefusesExisting` / `_OverwritesWithForce` / `_EmptyDir` / `_NonexistentDir`. +- [x] `internal/terraform/cli_test.go` — `TestPrintSummary_IncludesInitUpgradeHint` (captures stdout). +- [x] `go test ./internal/terraform/...` green. + +### Phase 7 — Opt-in smoke test (commit 7: `test(terraform): add terraform-validate smoke test behind build tag`) ✅ + +- [x] New file `internal/terraform/smoke_test.go` with `//go:build smoke` build tag at top. +- [x] In the file: build a representative `WorkspaceData` covering HTTP/TCP/DNS monitors (HTTP includes OTEL + assertions); slack + ms_teams + webhook-with-headers notifications; status page with theme/locales/allow_index + default_open group + monitor component; second status page with IP access. +- [x] Write all generated files to `t.TempDir()`. Skip the test (`t.Skipf`) if `terraform` is not on `PATH`. +- [x] Exec `terraform init -upgrade` then `terraform validate` against the temp dir; fail the test on non-zero exit. +- [x] Document at the top of the file: `// Run with: go test -tags=smoke ./internal/terraform/`. +- [x] `go test ./internal/terraform/...` (no tag) still green and does NOT invoke terraform. +- [x] Manually verified: `go test -tags=smoke ./internal/terraform/` passes locally. + +### Phase 8 — Version + docs (commit 8: `chore: bump cli to v1.1.0 and regenerate docs`) ✅ + +- [x] `internal/cmd/app.go` — change `Version: "v1.0.5"` → `Version: "v1.1.0"`. Also updated `internal/cmd/app_test.go` to match. +- [x] From repo root: `go run cmd/docs/docs.go` (updates `docs/openstatus-docs.md`). +- [x] `cd docs && pandoc -s -t man openstatus-docs.md -o openstatus.1` (updates the manpage). +- [x] Verified that `--force` flag entry appears in both `openstatus-docs.md` and `openstatus.1`. +- [x] `go build ./... && go vet ./... && go test ./...` all green. + +### Phase 9 — Submit ✅ + +- [x] Push the branch. _(Pushed `feat/tf-generate-sync-v0.2` to origin via `jj git push --bookmark feat/tf-generate-sync-v0.2 --allow-new`.)_ +- [ ] Open PR. Title suggestion: `terraform generate: sync with provider v0.2 (open_telemetry, ms_teams, ip access, theme/locales, --force)`. _(Author to open — GitHub provided: https://github.com/openstatusHQ/cli/pull/new/feat/tf-generate-sync-v0.2)_ +- [ ] PR description: bullet list of user-visible changes; include "Closes #…" if upstream has tracking issues; call out the two correctness fixes (webhook headers, IP access type) since they affect real users. +- [ ] Watch CI; address review feedback. +- [ ] Merge strategy is the author's call (repo allows squash, merge, rebase). + +### Definition of done + +- All boxes above checked. +- `go build ./...`, `go test ./...`, and `go test -tags=smoke ./internal/terraform/` all pass locally. +- The PR's diff on `internal/terraform/` matches the file-level summary in §6, plus the new `smoke_test.go`. +- `docs/openstatus-docs.md` and `docs/openstatus.1` show the `--force` flag. +- `internal/cmd/app.go` shows `v1.1.0`. +- No new TODOs, no commented-out code, no orphaned helpers (e.g. old `notificationProviderToString` removed if Phase 5 removed it).