diff --git a/apps/engineering/README.md b/apps/engineering/README.md index ec5b1f09ce..516601bebc 100644 --- a/apps/engineering/README.md +++ b/apps/engineering/README.md @@ -13,14 +13,14 @@ pnpm dev yarn dev ``` -Open http://localhost:3000 with your browser to see the result. +Open with your browser to see the result. ## Learn More To learn more about Next.js and Fumadocs, take a look at the following resources: -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js. features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs diff --git a/apps/engineering/app/docs/[[...slug]]/page.tsx b/apps/engineering/app/docs/[[...slug]]/page.tsx index 809e43320b..467bf2e49b 100644 --- a/apps/engineering/app/docs/[[...slug]]/page.tsx +++ b/apps/engineering/app/docs/[[...slug]]/page.tsx @@ -1,8 +1,9 @@ import { source } from "@/app/source"; +import { Banner } from "fumadocs-ui/components/banner"; +import { Tab, Tabs } from "fumadocs-ui/components/tabs"; import defaultMdxComponents from "fumadocs-ui/mdx"; import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/page"; import type { Metadata } from "next"; - import { notFound } from "next/navigation"; export default async function Page(props: { @@ -32,7 +33,14 @@ export default async function Page(props: { {page.data.description} - + ); diff --git a/apps/engineering/content/docs/architecture/meta.json b/apps/engineering/content/docs/architecture/meta.json index 3ee0ed7b57..b58cebb048 100644 --- a/apps/engineering/content/docs/architecture/meta.json +++ b/apps/engineering/content/docs/architecture/meta.json @@ -3,5 +3,5 @@ "description": "How does Unkey work", "icon": "Pencil", "root": false, - "pages": ["index", "---Services---"] + "pages": ["index", "services"] } diff --git a/apps/engineering/content/docs/architecture/services/meta.json b/apps/engineering/content/docs/architecture/services/meta.json index e18ef522de..a37713fdb8 100644 --- a/apps/engineering/content/docs/architecture/services/meta.json +++ b/apps/engineering/content/docs/architecture/services/meta.json @@ -2,5 +2,15 @@ "title": "Services", "icon": "Pencil", "root": false, - "pages": ["vault", "clickhouse", "clickhouse-proxy"] + "pages": [ + "api", + "clickhouse", + "clickhouse-proxy", + "deploy", + "healthcheck", + "quotacheck", + "run", + "vault", + "version" + ] } diff --git a/apps/engineering/content/docs/cli/deploy/index.mdx b/apps/engineering/content/docs/cli/deploy/index.mdx new file mode 100644 index 0000000000..8c23276116 --- /dev/null +++ b/apps/engineering/content/docs/cli/deploy/index.mdx @@ -0,0 +1,191 @@ +--- +title: "Deploy" +description: "Deploy a new version or initialize configuration" +--- +Build and deploy a new version of your application, or initialize configuration. + +The deploy command handles the complete deployment lifecycle: from building Docker images to deploying them on Unkey's infrastructure. It automatically detects your Git context, builds containers, and manages the deployment process with real-time status updates. + +## Initialization Mode + +Use --init to create a configuration template file. This generates an unkey.json file with your project settings, making future deployments simpler and more consistent across environments. + +## Deployment Process + +1. Load configuration from unkey.json or flags +2. Build Docker image from your application +3. Push image to container registry +4. Create deployment version on Unkey platform +5. Monitor deployment status until active + +## Command Syntax + +```bash +unkey deploy [flags] +``` + +## Examples + +### Initialize new project configuration + +```bash +unkey deploy --init +``` + +### Initialize with custom location + +```bash +unkey deploy --init --config=./my-project +``` + +### Force overwrite existing configuration + +```bash +unkey deploy --init --force +``` + +### Standard deployment (uses ./unkey.json) + +```bash +unkey deploy +``` + +### Deploy from specific config directory + +```bash +unkey deploy --config=./production +``` + +### Override workspace from config file + +```bash +unkey deploy --workspace-id=ws_production_123 +``` + +### Deploy with custom build context + +```bash +unkey deploy --context=./api +``` + +### Local development (build only, no push) + +```bash +unkey deploy --skip-push +``` + +### Deploy pre-built image + +```bash +unkey deploy --docker-image=ghcr.io/user/app:v1.0.0 +``` + +### Verbose output for debugging + +```bash +unkey deploy --verbose +``` + +## Flags + + +Directory containing unkey.json config file + +- **Type:** string + + + +Initialize configuration file in the specified directory + +- **Type:** boolean +- **Default:** `false` + + + +Force overwrite existing configuration file when using --init + +- **Type:** boolean +- **Default:** `false` + + + +Workspace ID + +- **Type:** string +- **Environment:** `UNKEY_WORKSPACE_ID` + + + +Project ID + +- **Type:** string +- **Environment:** `UNKEY_PROJECT_ID` + + + +Build context path + +- **Type:** string + + + +Git branch + +- **Type:** string +- **Default:** `"main"` + + + +Pre-built docker image + +- **Type:** string + + + +Path to Dockerfile + +- **Type:** string +- **Default:** `"Dockerfile"` + + + +Git commit SHA + +- **Type:** string + + + +Container registry + +- **Type:** string +- **Default:** `"ghcr.io/unkeyed/deploy"` +- **Environment:** `UNKEY_REGISTRY` + + + +Skip pushing to registry (for local testing) + +- **Type:** boolean +- **Default:** `false` + + + +Show detailed output for build and deployment operations + +- **Type:** boolean +- **Default:** `false` + + + +Control plane URL + +- **Type:** string +- **Default:** `"http://localhost:7091"` + + + +Control plane auth token + +- **Type:** string +- **Default:** `"ctrl-secret-token"` + diff --git a/apps/engineering/content/docs/cli/healthcheck/index.mdx b/apps/engineering/content/docs/cli/healthcheck/index.mdx new file mode 100644 index 0000000000..2a198b0b96 --- /dev/null +++ b/apps/engineering/content/docs/cli/healthcheck/index.mdx @@ -0,0 +1,35 @@ +--- +title: "Healthcheck" +description: "Perform an HTTP healthcheck against a given URL" +--- +This command sends an HTTP GET request to the specified URL and validates the response. It exits with code 0 if the server returns a 200 status code, otherwise exits with code 1. + +## Use Cases + +This is useful for health monitoring in CI/CD pipelines, service availability checks, load balancer health probes, and infrastructure monitoring scripts. + +## Command Syntax + +```bash +unkey healthcheck +``` + +## Examples + +### Check if a service is healthy + +```bash +unkey healthcheck https://api.unkey.dev/health +``` + +### Check local service + +```bash +unkey healthcheck http://localhost:8080/health +``` + +### Use in monitoring script + +```bash +unkey healthcheck https://example.com/api/status || echo 'Service is down!' +``` diff --git a/apps/engineering/content/docs/cli/index.mdx b/apps/engineering/content/docs/cli/index.mdx new file mode 100644 index 0000000000..04f70e2225 --- /dev/null +++ b/apps/engineering/content/docs/cli/index.mdx @@ -0,0 +1,4 @@ +--- +title: "CLI" +description: "Single binary for everything" +--- diff --git a/apps/engineering/content/docs/cli/meta.json b/apps/engineering/content/docs/cli/meta.json new file mode 100644 index 0000000000..7a765f3e75 --- /dev/null +++ b/apps/engineering/content/docs/cli/meta.json @@ -0,0 +1,6 @@ +{ + "title": "CLI", + "icon": "Pencil", + "root": false, + "pages": ["deploy", "healthcheck", "quotacheck", "run", "version"] +} diff --git a/apps/engineering/content/docs/cli/quotacheck/index.mdx b/apps/engineering/content/docs/cli/quotacheck/index.mdx new file mode 100644 index 0000000000..f8be219952 --- /dev/null +++ b/apps/engineering/content/docs/cli/quotacheck/index.mdx @@ -0,0 +1,64 @@ +--- +title: "Quotacheck" +description: "Check for exceeded quotas" +--- +Check for exceeded quotas and optionally send Slack notifications. + +This command monitors quota usage by querying ClickHouse for current usage metrics and comparing them against configured limits in the primary database. When quotas are exceeded, it can automatically send notifications via Slack webhook. + +## Configuration + +The command requires ClickHouse and database connections to function. Slack notifications are optional but recommended for production monitoring. + +## Command Syntax + +```bash +unkey quotacheck [flags] +``` + +## Examples + +### Check quotas without notifications + +```bash +unkey quotacheck --clickhouse-url clickhouse://localhost:9000 --database-dsn postgres://user:pass@localhost/db +``` + +### Check quotas with Slack notifications + +```bash +unkey quotacheck --clickhouse-url clickhouse://localhost:9000 --database-dsn postgres://user:pass@localhost/db --slack-webhook-url https://hooks.slack.com/services/... +``` + +### Using environment variables + +```bash +CLICKHOUSE_URL=... DATABASE_DSN=... SLACK_WEBHOOK_URL=... unkey quotacheck +``` + + +Some flags are required for this command to work properly. + + +## Flags + + +URL for the ClickHouse database + +- **Type:** string +- **Environment:** `CLICKHOUSE_URL` + + + +DSN for the primary database + +- **Type:** string +- **Environment:** `DATABASE_DSN` + + + +Slack webhook URL to send notifications + +- **Type:** string +- **Environment:** `SLACK_WEBHOOK_URL` + diff --git a/apps/engineering/content/docs/cli/run/api/index.mdx b/apps/engineering/content/docs/cli/run/api/index.mdx new file mode 100644 index 0000000000..7de41f974b --- /dev/null +++ b/apps/engineering/content/docs/cli/run/api/index.mdx @@ -0,0 +1,185 @@ +--- +title: "Api" +description: "Run the Unkey API server for validating and managing API keys" +--- + +## Command Syntax + +```bash +unkey run api [flags] +``` + + +Some flags are required for this command to work properly. + + +## Flags + + +HTTP port for the API server to listen on. Default: 7070 + +- **Type:** integer +- **Default:** `7070` +- **Environment:** `UNKEY_HTTP_PORT` + + + +Enable colored log output. Default: true + +- **Type:** boolean +- **Default:** `true` +- **Environment:** `UNKEY_LOGS_COLOR` + + + +Enable test mode. WARNING: Potentially unsafe, may trust client inputs blindly. Default: false + +- **Type:** boolean +- **Default:** `false` +- **Environment:** `UNKEY_TEST_MODE` + + + +Cloud platform identifier for this node. Used for logging and metrics. + +- **Type:** string +- **Environment:** `UNKEY_PLATFORM` + + + +Container image identifier. Used for logging and metrics. + +- **Type:** string +- **Environment:** `UNKEY_IMAGE` + + + +Geographic region identifier. Used for logging and routing. Default: unknown + +- **Type:** string +- **Default:** `"unknown"` +- **Environment:** `AWS_REGION` + + + +Unique identifier for this instance. Auto-generated if not provided. + +- **Type:** string +- **Default:** `"ins_65sRey"` +- **Environment:** `UNKEY_INSTANCE_ID` + + + +MySQL connection string for primary database. Required for all deployments. Example: user:pass@host:3306/unkey?parseTime=true + +- **Type:** string +- **Environment:** `UNKEY_DATABASE_PRIMARY` + + + +MySQL connection string for read-replica. Reduces load on primary database. Format same as database-primary. + +- **Type:** string +- **Environment:** `UNKEY_DATABASE_REPLICA` + + + +Redis connection string for rate-limiting and distributed counters. Example: redis://localhost:6379 + +- **Type:** string +- **Environment:** `UNKEY_REDIS_URL` + + + +ClickHouse connection string for analytics. Recommended for production. Example: clickhouse://user:pass@host:9000/unkey + +- **Type:** string +- **Environment:** `UNKEY_CLICKHOUSE_URL` + + + +Enable OpenTelemetry tracing and metrics + +- **Type:** boolean +- **Default:** `false` +- **Environment:** `UNKEY_OTEL` + + + +Sampling rate for OpenTelemetry traces (0.0-1.0). Only used when --otel is provided. Default: 0.25 + +- **Type:** float +- **Default:** `0.25` +- **Environment:** `UNKEY_OTEL_TRACE_SAMPLING_RATE` + + + +Enable Prometheus /metrics endpoint on specified port. Set to 0 to disable. + +- **Type:** integer +- **Environment:** `UNKEY_PROMETHEUS_PORT` + + + +Path to TLS certificate file for HTTPS. Both cert and key must be provided to enable HTTPS. + +- **Type:** string +- **Environment:** `UNKEY_TLS_CERT_FILE` + + + +Path to TLS key file for HTTPS. Both cert and key must be provided to enable HTTPS. + +- **Type:** string +- **Environment:** `UNKEY_TLS_KEY_FILE` + + + +Vault master keys for encryption + +- **Type:** string[] +- **Environment:** `UNKEY_VAULT_MASTER_KEYS` + + + +S3 Compatible Endpoint URL + +- **Type:** string +- **Environment:** `UNKEY_VAULT_S3_URL` + + + +S3 bucket name + +- **Type:** string +- **Environment:** `UNKEY_VAULT_S3_BUCKET` + + + +S3 access key ID + +- **Type:** string +- **Environment:** `UNKEY_VAULT_S3_ACCESS_KEY_ID` + + + +S3 secret access key + +- **Type:** string +- **Environment:** `UNKEY_VAULT_S3_SECRET_ACCESS_KEY` + + + +Enable ClickHouse proxy endpoints for high-throughput event collection + +- **Type:** boolean +- **Default:** `false` +- **Environment:** `UNKEY_CHPROXY_ENABLED` + + + +Authentication token for ClickHouse proxy endpoints. Required when proxy is enabled. + +- **Type:** string +- **Environment:** `UNKEY_CHPROXY_AUTH_TOKEN` + diff --git a/apps/engineering/content/docs/cli/run/ctrl/index.mdx b/apps/engineering/content/docs/cli/run/ctrl/index.mdx new file mode 100644 index 0000000000..35064c3f11 --- /dev/null +++ b/apps/engineering/content/docs/cli/run/ctrl/index.mdx @@ -0,0 +1,128 @@ +--- +title: "Ctrl" +description: "Run the Unkey control plane service for managing infrastructure and services" +--- + +## Command Syntax + +```bash +unkey run ctrl [flags] +``` + + +Some flags are required for this command to work properly. + + +## Flags + + +HTTP port for the control plane server to listen on. Default: 8080 + +- **Type:** integer +- **Default:** `8080` +- **Environment:** `UNKEY_HTTP_PORT` + + + +Enable colored log output. Default: true + +- **Type:** boolean +- **Default:** `true` +- **Environment:** `UNKEY_LOGS_COLOR` + + + +Cloud platform identifier for this node. Used for logging and metrics. + +- **Type:** string +- **Environment:** `UNKEY_PLATFORM` + + + +Container image identifier. Used for logging and metrics. + +- **Type:** string +- **Environment:** `UNKEY_IMAGE` + + + +Geographic region identifier. Used for logging and routing. Default: unknown + +- **Type:** string +- **Default:** `"unknown"` +- **Environment:** `AWS_REGION` + + + +Unique identifier for this instance. Auto-generated if not provided. + +- **Type:** string +- **Default:** `"ins_26qK8q"` +- **Environment:** `UNKEY_INSTANCE_ID` + + + +MySQL connection string for primary database. Required for all deployments. Example: user:pass@host:3306/unkey?parseTime=true + +- **Type:** string +- **Environment:** `UNKEY_DATABASE_PRIMARY` + + + +MySQL connection string for hydra database. Required for all deployments. Example: user:pass@host:3306/hydra?parseTime=true + +- **Type:** string +- **Environment:** `UNKEY_DATABASE_HYDRA` + + + +Enable OpenTelemetry tracing and metrics + +- **Type:** boolean +- **Default:** `false` +- **Environment:** `UNKEY_OTEL` + + + +Sampling rate for OpenTelemetry traces (0.0-1.0). Only used when --otel is provided. Default: 0.25 + +- **Type:** float +- **Default:** `0.25` +- **Environment:** `UNKEY_OTEL_TRACE_SAMPLING_RATE` + + + +Path to TLS certificate file for HTTPS. Both cert and key must be provided to enable HTTPS. + +- **Type:** string +- **Environment:** `UNKEY_TLS_CERT_FILE` + + + +Path to TLS key file for HTTPS. Both cert and key must be provided to enable HTTPS. + +- **Type:** string +- **Environment:** `UNKEY_TLS_KEY_FILE` + + + +Authentication token for control plane API access. Required for secure deployments. + +- **Type:** string +- **Environment:** `UNKEY_AUTH_TOKEN` + + + +Full URL of the metald service for VM operations. Required for deployments. Example: https://metald.example.com:8080 + +- **Type:** string +- **Environment:** `UNKEY_METALD_ADDRESS` + + + +Path to SPIFFE agent socket for mTLS authentication. Default: /var/lib/spire/agent/agent.sock + +- **Type:** string +- **Default:** `"/var/lib/spire/agent/agent.sock"` +- **Environment:** `UNKEY_SPIFFE_SOCKET_PATH` + diff --git a/apps/engineering/content/docs/cli/run/index.mdx b/apps/engineering/content/docs/cli/run/index.mdx new file mode 100644 index 0000000000..3aaae08d08 --- /dev/null +++ b/apps/engineering/content/docs/cli/run/index.mdx @@ -0,0 +1,22 @@ +--- +title: "Run" +description: "Run Unkey services" +--- +Run various Unkey services in development or production environments. + +This command starts different Unkey microservices. Each service can be configured independently and runs as a standalone process. + +## Available Services + +- api: The main API server for validating and managing API keys +- ctrl: The control plane service for managing infrastructure and deployments + +## Command Syntax + +```bash +unkey run +``` + +## Quick Reference +- `unkey run api` - Run the Unkey API server for validating and managing API keys +- `unkey run ctrl` - Run the Unkey control plane service for managing infrastructure and services \ No newline at end of file diff --git a/apps/engineering/content/docs/cli/version/get/index.mdx b/apps/engineering/content/docs/cli/version/get/index.mdx new file mode 100644 index 0000000000..64b85e79b8 --- /dev/null +++ b/apps/engineering/content/docs/cli/version/get/index.mdx @@ -0,0 +1,25 @@ +--- +title: "Get" +description: "Get details about a version" +--- +Get comprehensive details about a specific version including status, branch, creation time, and associated hostnames. + +## Command Syntax + +```bash +unkey version get +``` + +## Examples + +### Get details for a specific version + +```bash +unkey version get v_abc123def456 +``` + +### Get details for another version + +```bash +unkey version get v_def456ghi789 +``` diff --git a/apps/engineering/content/docs/cli/version/index.mdx b/apps/engineering/content/docs/cli/version/index.mdx new file mode 100644 index 0000000000..2fb4609491 --- /dev/null +++ b/apps/engineering/content/docs/cli/version/index.mdx @@ -0,0 +1,24 @@ +--- +title: "Version" +description: "Manage API versions" +--- +Create, list, and manage versions of your API. + +Versions are immutable snapshots of your code, configuration, and infrastructure settings. Each version represents a specific deployment state that can be rolled back to at any time. + +## Available Commands + +- get: Get details about a specific version +- list: List all versions with optional filtering +- rollback: Rollback to a previous version + +## Command Syntax + +```bash +unkey version +``` + +## Quick Reference +- `unkey version get` - Get details about a version +- `unkey version list` - List versions with optional filtering +- `unkey version rollback` - Rollback to a previous version \ No newline at end of file diff --git a/apps/engineering/content/docs/cli/version/list/index.mdx b/apps/engineering/content/docs/cli/version/list/index.mdx new file mode 100644 index 0000000000..2972423158 --- /dev/null +++ b/apps/engineering/content/docs/cli/version/list/index.mdx @@ -0,0 +1,68 @@ +--- +title: "List" +description: "List versions with optional filtering" +--- +List all versions for the current project with support for filtering by branch, status, and limiting results. + +## Filtering Options + +Use flags to filter results by branch name, status, or limit the number of results returned. Filters can be combined for more specific queries. + +## Command Syntax + +```bash +unkey version list [flags] +``` + +## Examples + +### List all versions + +```bash +unkey version list +``` + +### List versions from main branch + +```bash +unkey version list --branch main +``` + +### List only active versions + +```bash +unkey version list --status active +``` + +### List last 5 versions + +```bash +unkey version list --limit 5 +``` + +### Combine filters + +```bash +unkey version list --branch main --status active --limit 3 +``` + +## Flags + + +Filter by branch name + +- **Type:** string + + + +Filter by status (pending, building, active, failed) + +- **Type:** string + + + +Number of versions to show + +- **Type:** integer +- **Default:** `10` + diff --git a/apps/engineering/content/docs/cli/version/rollback/index.mdx b/apps/engineering/content/docs/cli/version/rollback/index.mdx new file mode 100644 index 0000000000..6819f2496c --- /dev/null +++ b/apps/engineering/content/docs/cli/version/rollback/index.mdx @@ -0,0 +1,44 @@ +--- +title: "Rollback" +description: "Rollback to a previous version" +--- +Rollback a hostname to a previous version. This operation will switch traffic from the current version to the specified target version. + +## Warning + +This operation affects live traffic. Use the --force flag to skip the confirmation prompt in automated environments. + +## Command Syntax + +```bash +unkey version rollback [flags] +``` + +## Examples + +### Rollback with confirmation prompt + +```bash +unkey version rollback my-api.unkey.app v_abc123def456 +``` + +### Rollback without confirmation for automation + +```bash +unkey version rollback my-api.unkey.app v_abc123def456 --force +``` + +### Rollback staging environment + +```bash +unkey version rollback staging-api.unkey.app v_def456ghi789 +``` + +## Flags + + +Skip confirmation prompt for automated deployments + +- **Type:** boolean +- **Default:** `false` + diff --git a/apps/engineering/content/docs/meta.json b/apps/engineering/content/docs/meta.json index 213faa5145..392bf67c88 100644 --- a/apps/engineering/content/docs/meta.json +++ b/apps/engineering/content/docs/meta.json @@ -7,6 +7,7 @@ "index", "api-design", "releases", + "cli", "architecture", "contributing", "infrastructure", diff --git a/apps/engineering/package.json b/apps/engineering/package.json index 2eb404a5fa..31dde5bcf5 100644 --- a/apps/engineering/package.json +++ b/apps/engineering/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { + "generate-docs": "./scripts/generate-cli-docs.sh", "build": "next build", "dev": "next dev", "start": "next start", diff --git a/apps/engineering/scripts/generate-cli-docs.sh b/apps/engineering/scripts/generate-cli-docs.sh new file mode 100755 index 0000000000..8160316c02 --- /dev/null +++ b/apps/engineering/scripts/generate-cli-docs.sh @@ -0,0 +1,100 @@ +#!/bin/bash +set -e + +# Build the binary first +echo "Building unkey binary..." +cd ../../go/ +make build +cd ../apps/engineering/ + +# Set docs directory relative to engineering/ +DOCS_DIR="./content/docs/cli" + +echo "Starting automated documentation generation..." +echo "DOCS_DIR: $DOCS_DIR" + +# Get available commands dynamically +echo "Discovering available commands..." +AVAILABLE_COMMANDS=$(../../go/unkey --help 2>/dev/null | awk '/COMMANDS:/{flag=1; next} /^$/{flag=0} flag && /^ [a-zA-Z]/ {gsub(/,.*/, "", $1); print $1}' | grep -v "help" | tr '\n' ' ') +echo "Found commands: $AVAILABLE_COMMANDS" + +if [ -z "$AVAILABLE_COMMANDS" ]; then + echo "ERROR: No commands found. Is the binary working?" + ../../go/unkey --help + exit 1 +fi + +total=0 +success=0 +generated_commands=() + +for cmd in $AVAILABLE_COMMANDS; do + echo "" + echo "=== Processing $cmd ===" + mkdir -p "$DOCS_DIR/$cmd" + total=$((total + 1)) + + if ../../go/unkey $cmd mdx >"$DOCS_DIR/$cmd/index.mdx" 2>/dev/null; then + echo "✓ Generated $DOCS_DIR/$cmd/index.mdx" + success=$((success + 1)) + generated_commands+=("$cmd") + else + echo "✗ Failed to generate $cmd" + continue + fi + + # Check for subcommands + help_output=$(../../go/unkey $cmd --help 2>/dev/null) + if echo "$help_output" | grep -q "COMMANDS:"; then + subcmds=$(echo "$help_output" | awk '/COMMANDS:/{flag=1; next} /^$/{flag=0} flag && /^ [a-zA-Z]/ {gsub(/,.*/, "", $1); print $1}' | grep -v "help") + subcount=$(echo "$subcmds" | wc -w) + echo " Found $subcount subcommands: $subcmds" + + for subcmd in $subcmds; do + if [ "$subcmd" != "" ]; then + mkdir -p "$DOCS_DIR/$cmd/$subcmd" + total=$((total + 1)) + if ../../go/unkey $cmd $subcmd mdx >"$DOCS_DIR/$cmd/$subcmd/index.mdx" 2>/dev/null; then + echo " ✓ Generated $DOCS_DIR/$cmd/$subcmd/index.mdx" + success=$((success + 1)) + else + echo " ✗ Failed to generate $cmd/$subcmd" + fi + fi + done + else + echo " No subcommands found" + fi +done + +# Update meta.json with generated commands +META_FILE="./content/docs/architecture/services/meta.json" +echo "" +echo "Updating $META_FILE..." + +# Read existing meta.json or create minimal structure +if [ -f "$META_FILE" ]; then + EXISTING_PAGES=$(jq -r '.pages // []' "$META_FILE") + META_BASE=$(jq 'del(.pages)' "$META_FILE") +else + EXISTING_PAGES='[]' + META_BASE='{"title": "Services", "icon": "Pencil", "root": false}' +fi + +# Convert generated commands to JSON array +GENERATED_JSON=$(printf '%s\n' "${generated_commands[@]}" | jq -R . | jq -s .) + +# Merge existing pages with generated commands and remove duplicates +COMBINED_PAGES=$(echo "$EXISTING_PAGES $GENERATED_JSON" | jq -s 'add | unique') + +# Create the complete meta.json +echo "$META_BASE" | jq --argjson pages "$COMBINED_PAGES" '. + {pages: $pages}' >"$META_FILE" + +echo "✓ Updated $META_FILE with $(echo "$COMBINED_PAGES" | jq 'length') total pages" +echo "" +echo "Summary: $success/$total files generated successfully" + +if [ $success -eq 0 ]; then + echo "ERROR: No documentation files were generated" + exit 1 +fi diff --git a/go/Makefile b/go/Makefile index 579124c14b..0418627ad6 100644 --- a/go/Makefile +++ b/go/Makefile @@ -42,4 +42,3 @@ build: generate: buf generate go generate ./... - diff --git a/go/cmd/deploy/main.go b/go/cmd/deploy/main.go index e3318e9acd..4477ae5334 100644 --- a/go/cmd/deploy/main.go +++ b/go/cmd/deploy/main.go @@ -119,55 +119,36 @@ var DeployFlags = []cli.Flag{ cli.String("auth-token", "Control plane auth token", cli.Default(DefaultAuthToken)), } +// WARNING: Changing the "Description" part will also affect generated MDX. // Cmd defines the deploy CLI command var Cmd = &cli.Command{ Name: "deploy", Usage: "Deploy a new version or initialize configuration", Description: `Build and deploy a new version of your application, or initialize configuration. -When used with --init, creates a configuration template file. -Otherwise, builds a container image from the specified context and -deploys it to the Unkey platform. +The deploy command handles the complete deployment lifecycle: from building Docker images to deploying them on Unkey's infrastructure. It automatically detects your Git context, builds containers, and manages the deployment process with real-time status updates. -The deploy command will automatically load configuration from unkey.json -in the current directory or specified config directory. +INITIALIZATION MODE: +Use --init to create a configuration template file. This generates an unkey.json file with your project settings, making future deployments simpler and more consistent across environments. -EXAMPLES: - # Initialize configuration file - unkey deploy --init - - # Initialize in specific directory - unkey deploy --init --config=./my-project - - # Force overwrite existing config - unkey deploy --init --force - - # Deploy using config file (./unkey.json) - unkey deploy - - # Deploy with config from specific directory - unkey deploy --config=./test-docker - - # Deploy overriding workspace from config - unkey deploy --workspace-id=ws_different +DEPLOYMENT PROCESS: +1. Load configuration from unkey.json or flags +2. Build Docker image from your application +3. Push image to container registry +4. Create deployment version on Unkey platform +5. Monitor deployment status until active - # Deploy with specific context (overrides config) - unkey deploy --context=./demo_api - - # Deploy with your own registry - unkey deploy \ - --workspace-id=ws_4QgQsKsKfdm3nGeC \ - --project-id=proj_9aiaks2dzl6mcywnxjf \ - --registry=docker.io/mycompany/myapp - - # Local development (skip push) - unkey deploy --skip-push - - # Deploy pre-built image - unkey deploy --docker-image=ghcr.io/user/app:v1.0.0 - - # Show detailed build and deployment output - unkey deploy --verbose`, +EXAMPLES: +unkey deploy --init # Initialize new project configuration +unkey deploy --init --config=./my-project # Initialize with custom location +unkey deploy --init --force # Force overwrite existing configuration +unkey deploy # Standard deployment (uses ./unkey.json) +unkey deploy --config=./production # Deploy from specific config directory +unkey deploy --workspace-id=ws_production_123 # Override workspace from config file +unkey deploy --context=./api # Deploy with custom build context +unkey deploy --skip-push # Local development (build only, no push) +unkey deploy --docker-image=ghcr.io/user/app:v1.0.0 # Deploy pre-built image +unkey deploy --verbose # Verbose output for debugging`, Flags: DeployFlags, Action: DeployAction, } diff --git a/go/cmd/healthcheck/main.go b/go/cmd/healthcheck/main.go index 5426576f0a..1a91a5757d 100644 --- a/go/cmd/healthcheck/main.go +++ b/go/cmd/healthcheck/main.go @@ -11,22 +11,18 @@ import ( var Cmd = &cli.Command{ Name: "healthcheck", Usage: "Perform an HTTP healthcheck against a given URL", - Description: `Perform an HTTP healthcheck against a given URL. -This command exits with 0 if the status code is 200, otherwise it exits with 1. + Description: `This command sends an HTTP GET request to the specified URL and validates the response. It exits with code 0 if the server returns a 200 status code, otherwise exits with code 1. -USAGE: - unkey healthcheck +USE CASES: +This is useful for health monitoring in CI/CD pipelines, service availability checks, load balancer health probes, and infrastructure monitoring scripts. EXAMPLES: - # Check if a service is healthy - unkey healthcheck https://api.unkey.dev/health - - # Check local service - unkey healthcheck http://localhost:8080/health`, +unkey healthcheck https://api.unkey.dev/health # Check if a service is healthy +unkey healthcheck http://localhost:8080/health # Check local service +unkey healthcheck https://example.com/api/status || echo 'Service is down!' # Use in monitoring script`, Action: runAction, } -// nolint:gocognit func runAction(ctx context.Context, cmd *cli.Command) error { args := cmd.Args() if len(args) == 0 { @@ -38,7 +34,6 @@ func runAction(ctx context.Context, cmd *cli.Command) error { return fmt.Errorf("you must provide a url like so: 'unkey healthcheck '") } - // nolint:gosec res, err := http.Get(url) if err != nil { return fmt.Errorf("failed to perform healthcheck: %w", err) diff --git a/go/cmd/quotacheck/main.go b/go/cmd/quotacheck/main.go index 5e439864e3..4e39cbbfca 100644 --- a/go/cmd/quotacheck/main.go +++ b/go/cmd/quotacheck/main.go @@ -18,9 +18,19 @@ import ( ) var Cmd = &cli.Command{ - Name: "quotacheck", - Usage: "Check for exceeded quotas", - Description: "Check for exceeded quotas and optionally send Slack notifications", + Name: "quotacheck", + Usage: "Check for exceeded quotas", + Description: `Check for exceeded quotas and optionally send Slack notifications. + +This command monitors quota usage by querying ClickHouse for current usage metrics and comparing them against configured limits in the primary database. When quotas are exceeded, it can automatically send notifications via Slack webhook. + +CONFIGURATION: +The command requires ClickHouse and database connections to function. Slack notifications are optional but recommended for production monitoring. + +EXAMPLES: +unkey quotacheck --clickhouse-url clickhouse://localhost:9000 --database-dsn postgres://user:pass@localhost/db # Check quotas without notifications +unkey quotacheck --clickhouse-url clickhouse://localhost:9000 --database-dsn postgres://user:pass@localhost/db --slack-webhook-url https://hooks.slack.com/services/... # Check quotas with Slack notifications +CLICKHOUSE_URL=... DATABASE_DSN=... SLACK_WEBHOOK_URL=... unkey quotacheck # Using environment variables`, Flags: []cli.Flag{ cli.String("clickhouse-url", "URL for the ClickHouse database", cli.EnvVar("CLICKHOUSE_URL"), cli.Required()), cli.String("database-dsn", "DSN for the primary database", cli.EnvVar("DATABASE_DSN"), cli.Required()), diff --git a/go/cmd/run/main.go b/go/cmd/run/main.go index 334daafe15..58c85503fd 100644 --- a/go/cmd/run/main.go +++ b/go/cmd/run/main.go @@ -12,19 +12,19 @@ import ( var Cmd = &cli.Command{ Name: "run", Usage: "Run Unkey services", - Description: `Run various Unkey services including: - - api: The main API server for validating and managing API keys - - ctrl: The control plane service for managing infrastructure + Description: `Run various Unkey services in development or production environments. -EXAMPLES: - # Run the API server - unkey run api +This command starts different Unkey microservices. Each service can be configured independently and runs as a standalone process. - # Run the control plane - unkey run ctrl +AVAILABLE SERVICES: +- api: The main API server for validating and managing API keys +- ctrl: The control plane service for managing infrastructure and deployments - # Show available services - unkey run --help`, +EXAMPLES: +unkey run api # Run the API server +unkey run ctrl # Run the control plane +unkey run --help # Show available services and their options +unkey run api --port 8080 --env production # Run API server with custom configuration`, Commands: []*cli.Command{ api.Cmd, ctrl.Cmd, diff --git a/go/cmd/version/main.go b/go/cmd/version/main.go index 3826dc4d13..55b8480b57 100644 --- a/go/cmd/version/main.go +++ b/go/cmd/version/main.go @@ -12,8 +12,13 @@ var Cmd = &cli.Command{ Name: "version", Usage: "Manage API versions", Description: `Create, list, and manage versions of your API. - -Versions are immutable snapshots of your code, configuration, and infrastructure settings.`, + +Versions are immutable snapshots of your code, configuration, and infrastructure settings. Each version represents a specific deployment state that can be rolled back to at any time. + +AVAILABLE COMMANDS: +- get: Get details about a specific version +- list: List all versions with optional filtering +- rollback: Rollback to a previous version`, Commands: []*cli.Command{ getCmd, listCmd, @@ -24,13 +29,11 @@ Versions are immutable snapshots of your code, configuration, and infrastructure var getCmd = &cli.Command{ Name: "get", Usage: "Get details about a version", - Description: `Get details about a specific version. - -USAGE: - unkey version get + Description: `Get comprehensive details about a specific version including status, branch, creation time, and associated hostnames. EXAMPLES: - unkey version get v_abc123def456`, +unkey version get v_abc123def456 # Get details for a specific version +unkey version get v_def456ghi789 # Get details for another version`, Action: func(ctx context.Context, cmd *cli.Command) error { logger := slog.Default() @@ -56,9 +59,20 @@ EXAMPLES: var listCmd = &cli.Command{ Name: "list", - Usage: "List versions", + Usage: "List versions with optional filtering", + Description: `List all versions for the current project with support for filtering by branch, status, and limiting results. + +FILTERING OPTIONS: +Use flags to filter results by branch name, status, or limit the number of results returned. Filters can be combined for more specific queries. + +EXAMPLES: +unkey version list # List all versions +unkey version list --branch main # List versions from main branch +unkey version list --status active # List only active versions +unkey version list --limit 5 # List last 5 versions +unkey version list --branch main --status active --limit 3 # Combine filters`, Flags: []cli.Flag{ - cli.String("branch", "Filter by branch"), + cli.String("branch", "Filter by branch name"), cli.String("status", "Filter by status (pending, building, active, failed)"), cli.Int("limit", "Number of versions to show", cli.Default(10)), }, @@ -81,16 +95,17 @@ var listCmd = &cli.Command{ var rollbackCmd = &cli.Command{ Name: "rollback", Usage: "Rollback to a previous version", - Description: `Rollback to a previous version. + Description: `Rollback a hostname to a previous version. This operation will switch traffic from the current version to the specified target version. -USAGE: - unkey version rollback +WARNING: +This operation affects live traffic. Use the --force flag to skip the confirmation prompt in automated environments. EXAMPLES: - unkey version rollback my-api.unkey.app v_abc123def456 - unkey version rollback my-api.unkey.app v_abc123def456 --force`, +unkey version rollback my-api.unkey.app v_abc123def456 # Rollback with confirmation prompt +unkey version rollback my-api.unkey.app v_abc123def456 --force # Rollback without confirmation for automation +unkey version rollback staging-api.unkey.app v_def456ghi789 # Rollback staging environment`, Flags: []cli.Flag{ - cli.Bool("force", "Skip confirmation prompt"), + cli.Bool("force", "Skip confirmation prompt for automated deployments"), }, Action: func(ctx context.Context, cmd *cli.Command) error { logger := slog.Default() diff --git a/go/pkg/cli/command.go b/go/pkg/cli/command.go index c09013bddd..0ce764c43e 100644 --- a/go/pkg/cli/command.go +++ b/go/pkg/cli/command.go @@ -1,3 +1,5 @@ +// Package cli provides a command-line interface framework for building CLI applications. +// It supports nested commands, various flag types, and structured error handling. package cli import ( @@ -28,6 +30,7 @@ type Command struct { Flags []Flag // Available flags for this command Action Action // Function to execute when command is run Aliases []string // Alternative names for this command + commandPath string // Full command path for MDX generation (e.g., "run api") // Runtime state (populated during parsing) args []string // Non-flag arguments passed to command @@ -182,6 +185,10 @@ func (c *Command) Run(ctx context.Context, args []string) error { if len(args) == 0 { return ErrNoArguments } + // Handle MDX generation first + if handled, err := c.handleMDXGeneration(args); handled { + return err + } // Parse arguments starting from index 1 (skip program name) return c.parse(ctx, args[1:]) } diff --git a/go/pkg/cli/docs.go b/go/pkg/cli/docs.go new file mode 100644 index 0000000000..e59888591c --- /dev/null +++ b/go/pkg/cli/docs.go @@ -0,0 +1,375 @@ +package cli + +import ( + "errors" + "fmt" + "regexp" + "strings" + "text/template" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +var ( + ErrCommandNil = errors.New("command cannot be nil") + ErrTemplateParseFailure = errors.New("failed to parse MDX template") + ErrTemplateExecFailure = errors.New("failed to execute MDX template") +) + +// FrontMatter holds metadata for the MDX file +type FrontMatter struct { + Title string + Description string +} + +// GenerateMDX creates Fuma docs MDX from any command metadata with frontmatter +func (c *Command) GenerateMDX(frontMatter *FrontMatter) (string, error) { + if c == nil { + return "", ErrCommandNil + } + + data := c.extractMDXData(frontMatter) + + // Choose template based on whether this is a parent command or leaf command + var templateStr string + if len(c.Commands) > 0 { + templateStr = parentCommandTemplate + } else { + templateStr = leafCommandTemplate + } + + caser := cases.Title(language.English) + tmpl, err := template.New("mdx").Funcs(template.FuncMap{ + "join": strings.Join, + "contains": strings.Contains, + "hasPrefix": strings.HasPrefix, + "title": caser.String, + "lower": strings.ToLower, + "hasItems": func(items any) bool { return c.hasItems(items) }, + "eq": func(a, b string) bool { return a == b }, + }).Parse(templateStr) + if err != nil { + return "", fmt.Errorf("%w: %v", ErrTemplateParseFailure, err) + } + + var result strings.Builder + err = tmpl.Execute(&result, data) + if err != nil { + return "", fmt.Errorf("%w: %v", ErrTemplateExecFailure, err) + } + + return result.String(), nil +} + +// MDXData holds structured command data for template generation +type MDXData struct { + Name string + Usage string + Description string + HasSubcmds bool + Subcommands []MDXSubcommand + Examples []MDXExample + Flags []MDXFlag + CommandType string + FrontMatter *FrontMatter + CommandPath string +} + +type MDXSubcommand struct { + Name string + Usage string + Description string + Aliases []string +} + +type MDXExample struct { + Title string + Command string + Comment string +} + +type MDXFlag struct { + Name string + Description string + Type string + Default string + EnvVar string + Required bool +} + +// extractMDXData parses command metadata into structured data +func (c *Command) extractMDXData(frontMatter *FrontMatter) MDXData { + return MDXData{ + Name: c.Name, + Usage: c.Usage, + Description: c.getCleanDescription(), + HasSubcmds: len(c.Commands) > 0, + Subcommands: c.extractSubcommands(), + Examples: c.extractExamples(), + Flags: c.extractFlags(), + CommandType: c.determineCommandType(), + FrontMatter: frontMatter, + CommandPath: c.getCommandPath(), + } +} + +// getCleanDescription returns description without EXAMPLES section for MDX +// and converts UPPERCASE sections to proper markdown headings +func (c *Command) getCleanDescription() string { + if c.Description == "" { + return c.Usage + } + + // Remove EXAMPLES section for cleaner MDX description + exampleRegex := regexp.MustCompile(`(?s)\nEXAMPLES:.*`) + cleaned := exampleRegex.ReplaceAllString(c.Description, "") + + // Convert UPPERCASE SECTIONS: to ## headings + headingRegex := regexp.MustCompile(`\n([A-Z][A-Z\s]+):\s*\n`) + cleaned = headingRegex.ReplaceAllStringFunc(cleaned, func(match string) string { + // Extract the heading text + parts := headingRegex.FindStringSubmatch(match) + if len(parts) > 1 { + heading := strings.TrimSpace(parts[1]) + // Convert to title case for better readability + caser := cases.Title(language.English) + heading = caser.String(strings.ToLower(heading)) + return fmt.Sprintf("\n## %s\n\n", heading) + } + return match + }) + + return strings.TrimSpace(cleaned) +} + +// extractSubcommands gets subcommand information +func (c *Command) extractSubcommands() []MDXSubcommand { + var subcmds []MDXSubcommand + + for _, cmd := range c.Commands { + subcmds = append(subcmds, MDXSubcommand{ + Name: cmd.Name, + Usage: cmd.Usage, + Description: c.extractFirstSentence(cmd.Description), + Aliases: cmd.Aliases, + }) + } + + return subcmds +} + +// extractFirstSentence gets the first sentence of a description for use in summary contexts +func (c *Command) extractFirstSentence(desc string) string { + if desc == "" { + return "" + } + + // Split on sentence-ending punctuation followed by whitespace + sentences := regexp.MustCompile(`[.!?]\s+`).Split(desc, 2) + if len(sentences) > 0 { + first := strings.TrimSpace(sentences[0]) + if first != "" && !strings.HasSuffix(first, ".") { + first += "." + } + return first + } + + // Fallback: use first line if no sentence punctuation found + lines := strings.Split(desc, "\n") + if len(lines) > 0 { + return strings.TrimSpace(lines[0]) + } + + return desc +} + +// extractAllExamples parses examples from a dedicated EXAMPLES section in the command description +// Looks for a section starting with "EXAMPLES:" and extracts command lines that contain the command name +// This allows embedding rich examples directly in the CLI source code that get formatted for documentation +// Example description format: +// +// "Deploy applications to Unkey infrastructure. +// +// EXAMPLES: +// unkey deploy --init # Initialize configuration +// unkey deploy --verbose # Deploy with detailed output +// unkey deploy --skip-push # Local testing only" +func (c *Command) extractExamples() []MDXExample { + if c.Description == "" { + return nil + } + + // Find EXAMPLES section + exampleRegex := regexp.MustCompile(`(?s)EXAMPLES:\s*(.*?)(?:\n[A-Z][A-Z\s]*:|\z)`) + matches := exampleRegex.FindStringSubmatch(c.Description) + if len(matches) < 2 { + return nil + } + + var examples []MDXExample + // Parse example lines from the EXAMPLES section + lines := strings.SplitSeq(matches[1], "\n") + + for line := range lines { + line = strings.TrimSpace(line) + + // Skip empty lines and comment-only lines + if line == "" || strings.HasPrefix(line, "#") { + continue + } + + // Only process lines that contain the command name + if strings.Contains(line, c.Name) { + example := MDXExample{ + Command: c.cleanCommand(line), + Comment: c.extractComment(line), + Title: c.generateTitle(line), + } + examples = append(examples, example) + } + } + + return examples +} + +// cleanCommand removes comments and cleans up command line +func (c *Command) cleanCommand(line string) string { + if line == "" { + return "" + } + + // Remove inline comments + if idx := strings.Index(line, "#"); idx != -1 { + line = line[:idx] + } + + return strings.TrimSpace(line) +} + +// extractComment pulls out inline comments from command examples +func (c *Command) extractComment(line string) string { + if line == "" { + return "" + } + + idx := strings.Index(line, "#") + if idx == -1 { + return "" + } + + return strings.TrimSpace(line[idx+1:]) +} + +// generateTitle creates meaningful titles for examples +func (c *Command) generateTitle(command string) string { + if command == "" { + return "Basic usage" + } + + // Always prioritize comment as title if present + if comment := c.extractComment(command); comment != "" { + // Capitalize first letter + if len(comment) > 0 { + return strings.ToUpper(comment[:1]) + comment[1:] + } + } + + // Simple fallback based on complexity + cleanCmd := c.cleanCommand(command) + flagCount := strings.Count(cleanCmd, "--") + + if flagCount == 0 { + return "Basic usage" + } + if flagCount == 1 { + return "Basic configuration" + } + if flagCount <= 3 { + return "With options" + } + + return "Advanced configuration" +} + +// Get full command path +func (c *Command) getCommandPath() string { + if c.commandPath != "" { + return c.commandPath + } + return c.Name +} + +// extractFlags to include short names and aliases +func (c *Command) extractFlags() []MDXFlag { + if len(c.Flags) == 0 { + return nil + } + + var flags []MDXFlag + + for _, flag := range c.Flags { + if flag == nil { + continue + } + + mdxFlag := MDXFlag{ + Name: flag.Name(), + Description: flag.Usage(), + Type: c.getTypeString(flag), + Default: c.getDefaultValue(flag), + EnvVar: c.getEnvVar(flag), + Required: flag.Required(), + } + flags = append(flags, mdxFlag) + } + + return flags +} + +// getTypeString returns the type name for a flag +func (c *Command) getTypeString(flag Flag) string { + if flag == nil { + return "unknown" + } + + switch flag.(type) { + case *StringFlag: + return "string" + case *BoolFlag: + return "boolean" + case *IntFlag: + return "integer" + case *FloatFlag: + return "float" + case *StringSliceFlag: + return "string[]" + default: + return "unknown" + } +} + +// determineCommandType categorizes the command for template selection +func (c *Command) determineCommandType() string { + if len(c.Commands) > 0 { + return "parent" + } + if c.Action != nil { + return "leaf" + } + return "service" +} + +// hasItems checks if any slice has items for template conditionals +func (c *Command) hasItems(items any) bool { + switch v := items.(type) { + case []MDXFlag: + return len(v) > 0 + case []MDXExample: + return len(v) > 0 + case []MDXSubcommand: + return len(v) > 0 + default: + return false + } +} diff --git a/go/pkg/cli/docs_handler.go b/go/pkg/cli/docs_handler.go new file mode 100644 index 0000000000..a7330d9fb4 --- /dev/null +++ b/go/pkg/cli/docs_handler.go @@ -0,0 +1,122 @@ +package cli + +import ( + "errors" + "fmt" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +var ( + ErrCommandNotFound = errors.New("command not found") + ErrMDXGeneration = errors.New("failed to generate MDX") +) + +// handleMDXGeneration checks if this is an MDX generation request and handles it +// Update handleMDXGeneration to pass command path to GenerateMDX +func (c *Command) handleMDXGeneration(args []string) (bool, error) { + if len(args) == 0 { + return false, nil + } + if c == nil { + return false, ErrCommandNil + } + + lastArg := args[len(args)-1] + if lastArg != "mdx" { + return false, nil // Not an MDX request + } + + // Build the command path from args (excluding program name and "mdx") + commandPath := args[1 : len(args)-1] // Skip program name and "mdx" + + // Find the target command by traversing the command tree + targetCmd := c.findCommandByPath(commandPath) + if targetCmd == nil { + return true, fmt.Errorf("%w: '%s'", ErrCommandNotFound, strings.Join(commandPath, " ")) + } + + // Generate appropriate frontmatter based on command path + frontMatter := c.generateFrontMatterFromPath(commandPath, targetCmd) + + // Set the command path on the target command before generating MDX + targetCmd.commandPath = strings.Join(commandPath, " ") + + // Generate and output MDX + mdxContent, err := targetCmd.GenerateMDX(frontMatter) + if err != nil { + return true, fmt.Errorf("%w: %v", ErrMDXGeneration, err) + } + + fmt.Print(mdxContent) + return true, nil +} + +// findCommandByPath traverses the command tree to find the target command +func (c *Command) findCommandByPath(path []string) *Command { + if c == nil { + return nil + } + if len(path) == 0 { + return c + } + + // Skip the first element if it matches current command name + searchPath := path + if len(path) > 0 && path[0] == c.Name { + searchPath = path[1:] + } + + if len(searchPath) == 0 { + return c + } + + // Find the next command in the path + for _, cmd := range c.Commands { + if cmd == nil { + continue + } + if cmd.Name == searchPath[0] { + return cmd.findCommandByPath(searchPath[1:]) + } + } + + return nil +} + +// generateFrontMatterFromPath creates appropriate frontmatter based on command path +func (c *Command) generateFrontMatterFromPath(path []string, targetCmd *Command) *FrontMatter { + if c == nil || targetCmd == nil { + return &FrontMatter{ + Title: "Unknown Command", + Description: "No description available", + } + } + + caser := cases.Title(language.English) + + if len(path) == 0 { + return &FrontMatter{ + Title: caser.String(c.Name), + Description: c.Usage, + } + } + + // Build title from command path + title := "" + if len(path) > 0 && path[len(path)-1] != "" { + title = caser.String(path[len(path)-1]) + } + + description := targetCmd.Usage + if description == "" { + description = "No description available" + } + + return &FrontMatter{ + Title: title, + Description: description, + } +} diff --git a/go/pkg/cli/docs_template.go b/go/pkg/cli/docs_template.go new file mode 100644 index 0000000000..9f83e7eef5 --- /dev/null +++ b/go/pkg/cli/docs_template.go @@ -0,0 +1,132 @@ +package cli + +// Template for parent commands (commands with subcommands) +const parentCommandTemplate = `--- +{{- if .FrontMatter }} +title: "{{ .FrontMatter.Title }}" +description: "{{ .FrontMatter.Description }}" +{{- else }} +title: "{{ .Name | title }}" +description: "{{ .Usage }}" +{{- end }} +--- + +{{- if .Description }} +{{ .Description }} +{{- else }} +{{ .Usage }} +{{- end }} + +## Command Syntax + +` + "```bash" + ` +unkey {{ .CommandPath }}{{- if hasItems .Flags }} [flags]{{- end }} +` + "```" + ` + +## Quick Reference +{{- range .Subcommands }} +- ` + "`unkey {{ $.CommandPath }} {{ .Name }}`" + ` - {{ .Usage }} +{{- end }} + +{{- if hasItems .Flags }} +{{- $hasRequired := false }} +{{- range .Flags }}{{- if .Required }}{{- $hasRequired = true }}{{- end }}{{- end }} +{{- if $hasRequired }} + + +Some flags are required for this command to work properly. + + +{{- end }} + +## Global Flags + +These flags apply to all subcommands: + +{{- range .Flags }} + + +{{ .Description }} + +- **Type:** {{ .Type }} +{{- if .Default }} +- **Default:** ` + "`{{ .Default }}`" + ` +{{- end }} +{{- if .EnvVar }} +- **Environment:** ` + "`{{ .EnvVar }}`" + ` +{{- end }} + + +{{- end }} +{{- end }}` + +// Template for leaf commands (commands without subcommands) +const leafCommandTemplate = `--- +{{- if .FrontMatter }} +title: "{{ .FrontMatter.Title }}" +description: "{{ .FrontMatter.Description }}" +{{- else }} +title: "{{ .Name | title }}" +description: "{{ .Usage }}" +{{- end }} +--- + +{{- if and .Description (ne .Description .Usage) }} +{{ .Description }} +{{- else if and .Description (eq .Description .Usage) }} +{{- /* Skip printing description if it's the same as usage */}} +{{- else if .Usage }} +{{ .Usage }} +{{- end }} + +## Command Syntax + +` + "```bash" + ` +unkey {{ .CommandPath }}{{- if hasItems .Flags }} [flags]{{- end }} +` + "```" + ` + +{{- if hasItems .Examples }} + +## Examples + +{{- range .Examples }} + +### {{ .Title }} + +` + "```bash" + ` +{{ .Command }} +` + "```" + ` + +{{- end }} +{{- end }} + +{{- if hasItems .Flags }} +{{- $hasRequired := false }} +{{- range .Flags }}{{- if .Required }}{{- $hasRequired = true }}{{- end }}{{- end }} +{{- if $hasRequired }} + + +Some flags are required for this command to work properly. + + +{{- end }} + +## Flags + +{{- range .Flags }} + + +{{ .Description }} + +- **Type:** {{ .Type }} +{{- if .Default }} +- **Default:** ` + "`{{ .Default }}`" + ` +{{- end }} +{{- if .EnvVar }} +- **Environment:** ` + "`{{ .EnvVar }}`" + ` +{{- end }} + + +{{- end }} +{{- end }} +`