From 023f6aa52bef8017e6a4b391e2fe3bf04cc1235e Mon Sep 17 00:00:00 2001 From: Nicolas Ritouet Date: Sun, 15 Feb 2026 22:19:43 +0100 Subject: [PATCH 1/5] docs: update MCP server docs with 4 missing tools Remove keyway_get_secret (no longer exists), add keyway_generate, keyway_diff, keyway_scan, and keyway_validate with parameter tables and example prompts. Also update docs CLAUDE.md directory listing with 3 missing pages (installation, ai-agents, organizations). Co-Authored-By: Claude Opus 4.6 --- packages/docs/CLAUDE.md | 5 +- packages/docs/docs/mcp.md | 114 ++++++++++++++++++++++++++++++++------ 2 files changed, 100 insertions(+), 19 deletions(-) diff --git a/packages/docs/CLAUDE.md b/packages/docs/CLAUDE.md index 564df3e..0209c12 100644 --- a/packages/docs/CLAUDE.md +++ b/packages/docs/CLAUDE.md @@ -22,11 +22,14 @@ pnpm run clear # Clear Docusaurus cache ``` docs/ ├── intro.md # Getting started (landing page) +├── installation.md # Installation guide ├── cli.md # CLI reference ├── api.md # API reference ├── ci-cd.md # CI/CD integration -├── mcp.md # MCP Server for AI tools +├── mcp.md # MCP Server for AI tools (9 tools) +├── ai-agents.md # AI agents integration (keyway run) ├── security.md # Security & permissions +├── organizations.md # Organizations & billing └── integrations.md # Provider integrations (Vercel, etc.) ``` diff --git a/packages/docs/docs/mcp.md b/packages/docs/docs/mcp.md index 6e1dfe6..02e9a48 100644 --- a/packages/docs/docs/mcp.md +++ b/packages/docs/docs/mcp.md @@ -86,26 +86,19 @@ See [Installation](/installation) if you haven't installed the CLI yet. ### keyway_list_secrets -List secret names (without values). +List all secret names in the vault for the current repository. Returns only the keys, not the values. ```json { "environment": "production" } ``` -### keyway_get_secret - -Get a specific secret value. - -```json -{ - "name": "DATABASE_URL", - "environment": "production" -} -``` +| Parameter | Required | Description | +|-----------|----------|-------------| +| `environment` | No | Environment to list secrets from (default: `development`) | ### keyway_set_secret -Create or update a secret. +Create or update a secret in the vault. ```json { @@ -115,9 +108,15 @@ Create or update a secret. } ``` +| Parameter | Required | Description | +|-----------|----------|-------------| +| `name` | Yes | Secret name — must be UPPERCASE_WITH_UNDERSCORES | +| `value` | Yes | Secret value to store | +| `environment` | No | Environment to set secret in (default: `development`) | + ### keyway_inject_run -Run a command with secrets as env vars. +Run a command with secrets injected as environment variables. Secrets are only available to this command and are never written to disk. ```json { @@ -130,7 +129,7 @@ Run a command with secrets as env vars. | Parameter | Required | Description | |-----------|----------|-------------| -| `command` | Yes | Executable to run | +| `command` | Yes | Executable to run (e.g., `npm`, `python`) | | `args` | No | Array of arguments | | `environment` | No | Vault environment (default: `development`) | | `timeout` | No | Max runtime in milliseconds (default: 300000 = 5min) | @@ -139,12 +138,82 @@ The command runs with `shell: false` to prevent shell injection. Secret values a ### keyway_list_environments -List available environments. +List available environments for the current repository vault. ```json {} ``` +### keyway_generate + +Generate a secure secret and store it directly in the vault. The secret value is never exposed in the AI conversation. + +```json +{ + "name": "JWT_SECRET", + "type": "jwt-secret", + "length": 64, + "environment": "production" +} +``` + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `name` | Yes | Secret name — must be UPPERCASE_WITH_UNDERSCORES | +| `type` | No | Type of secret: `password`, `uuid`, `api-key`, `jwt-secret`, `hex`, `base64` (default: `password`) | +| `length` | No | Length of the secret, 8–256 (default: 32) | +| `environment` | No | Environment to store the secret in (default: `development`) | + +### keyway_diff + +Compare secrets between two environments to find differences. Shows which keys are missing, added, or have different values (without revealing the values). + +```json +{ + "env1": "development", + "env2": "production" +} +``` + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `env1` | Yes | First environment (e.g., `development`) | +| `env2` | Yes | Second environment (e.g., `production`) | + +### keyway_scan + +Scan the codebase for potential secret leaks. Detects AWS keys, GitHub tokens, Stripe keys, private keys, and more. + +```json +{ + "path": ".", + "exclude": ["fixtures"] +} +``` + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `path` | No | Path to scan (default: current directory) | +| `exclude` | No | Additional directories to exclude from scanning | + +### keyway_validate + +Validate that required secrets exist in an environment. Useful for pre-deployment checks. Can auto-detect required secrets from your codebase. + +```json +{ + "environment": "production", + "autoDetect": true +} +``` + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `environment` | Yes | Environment to validate (e.g., `production`) | +| `required` | No | List of required secret names to check | +| `autoDetect` | No | Auto-detect required secrets from codebase (default: `false`) | +| `path` | No | Path to scan for auto-detection (default: current directory) | + --- ## Example Prompts @@ -152,15 +221,24 @@ List available environments. **"What secrets are in production?"** → Uses `keyway_list_secrets` -**"Get the DATABASE_URL for staging"** -→ Uses `keyway_get_secret` - **"Run the tests with the development secrets"** → Uses `keyway_inject_run` **"Add a new API_KEY secret with value xyz"** → Uses `keyway_set_secret` +**"Generate a secure JWT secret for production"** +→ Uses `keyway_generate` + +**"What's different between staging and production?"** +→ Uses `keyway_diff` + +**"Scan the codebase for leaked secrets"** +→ Uses `keyway_scan` + +**"Are all required secrets set for production?"** +→ Uses `keyway_validate` + --- ## Security From 43bece97b155052d5ccfa43430d479dbb760d28f Mon Sep 17 00:00:00 2001 From: Nicolas Ritouet Date: Mon, 16 Feb 2026 01:18:20 +0100 Subject: [PATCH 2/5] docs: add self-hosting guide Complete documentation for deploying Keyway on your own infrastructure with Docker Compose, including DNS setup, GitHub App creation, environment configuration, architecture diagram, optional services, GitHub Enterprise support, and troubleshooting. Co-Authored-By: Claude Opus 4.6 --- packages/docs/docs/intro.md | 1 + packages/docs/docs/self-hosting.md | 405 +++++++++++++++++++++++++++++ packages/docs/sidebars.ts | 1 + 3 files changed, 407 insertions(+) create mode 100644 packages/docs/docs/self-hosting.md diff --git a/packages/docs/docs/intro.md b/packages/docs/docs/intro.md index 5e39d45..0a7aef3 100644 --- a/packages/docs/docs/intro.md +++ b/packages/docs/docs/intro.md @@ -138,3 +138,4 @@ Upgrade: [keyway.sh/settings](https://keyway.sh/settings) - [API](/api) - REST API and API keys - [Organizations](/organizations) - Team billing and management - [Security](/security) - Permissions and activity logs +- [Self-Hosting](/self-hosting) - Deploy on your own infrastructure diff --git a/packages/docs/docs/self-hosting.md b/packages/docs/docs/self-hosting.md new file mode 100644 index 0000000..9097621 --- /dev/null +++ b/packages/docs/docs/self-hosting.md @@ -0,0 +1,405 @@ +--- +sidebar_position: 11 +title: Self-Hosting +--- + +# Self-Hosting + +Deploy Keyway on your own infrastructure with Docker Compose. Your encryption keys, your servers, your rules. + +## Prerequisites + +- **Docker** and **Docker Compose** v2+ +- A **domain name** with DNS access (e.g., `example.com`) +- A **GitHub App** (created below) +- Ports **80** and **443** open for HTTPS + +## Quick Start + +```bash +# 1. Clone the repo +git clone https://github.com/keywaysh/keyway.git +cd keyway + +# 2. Configure +cp .env.example .env +# Edit .env — fill in DOMAIN, ENCRYPTION_KEY, JWT_SECRET, and GitHub App values + +# 3. Deploy +docker compose up --build -d +``` + +Your instance will be available at: +- **Dashboard:** `https://app.your-domain.com` +- **API:** `https://api.your-domain.com` + +--- + +## Step 1: DNS Setup + +Create DNS A records pointing to your server: + +| Record | Type | Value | +|--------|------|-------| +| `example.com` | A | `` | +| `app.example.com` | A | `` | +| `api.example.com` | A | `` | + +Caddy automatically provisions Let's Encrypt TLS certificates for these domains. + +## Step 2: Create a GitHub App + +Keyway requires a GitHub App for authentication and repository access verification. + +1. Go to **https://github.com/settings/apps/new** (or your GitHub Enterprise instance) + +2. Fill in the form: + + | Field | Value | + |-------|-------| + | **App name** | `keyway` (or any unique name) | + | **Homepage URL** | `https://example.com` | + | **Callback URL** | `https://api.example.com/v1/auth/callback` | + | **Request user authorization during installation** | Checked | + | **Enable Device Flow** | Checked | + | **Webhook URL** | `https://api.example.com/v1/github/webhook` (optional) | + | **Webhook secret** | Generate a random string (optional) | + +3. Set permissions: + + | Scope | Permission | + |-------|------------| + | **Repository: Metadata** | Read-only | + | **Repository: Administration** | Read-only | + | **Account: Email addresses** | Read-only | + +4. Subscribe to events (optional, for real-time sync): + - **Installation** + - **Installation repositories** + +5. After creating, note down: + - **App ID** (visible on the app settings page) + - **Client ID** + - **Client Secret** (generate one) + - **Private Key** (generate and download the `.pem` file) + +6. Base64-encode the private key for the `.env` file: + ```bash + cat your-app.private-key.pem | base64 | tr -d '\n' + ``` + +## Step 3: Configure Environment + +Copy `.env.example` to `.env` and fill in the required values: + +```bash +cp .env.example .env +``` + +### Required Variables + +```bash +# Your domain +DOMAIN=example.com + +# Encryption key (CANNOT be changed after secrets are stored) +ENCRYPTION_KEY= # openssl rand -hex 32 + +# JWT secret +JWT_SECRET= # openssl rand -base64 32 + +# GitHub App credentials (from Step 2) +GITHUB_APP_ID=123456 +GITHUB_APP_CLIENT_ID=Iv1.abc123 +GITHUB_APP_CLIENT_SECRET=abc123... +GITHUB_APP_PRIVATE_KEY=base64encodedkey... +GITHUB_APP_NAME=keyway +``` + +:::tip +`make setup` can auto-generate `ENCRYPTION_KEY` and `JWT_SECRET` for you. +::: + +### URL Defaults + +All URLs are derived from `DOMAIN` automatically: + +| Service | URL | +|---------|-----| +| Dashboard | `https://app.` | +| API | `https://api.` | + +Override individual URLs if needed: +```bash +DASHBOARD_URL=https://custom-dashboard.example.com +ALLOWED_ORIGINS=https://custom-dashboard.example.com +``` + +## Step 4: Deploy + +```bash +docker compose up --build -d +``` + +This starts all services: + +| Service | Description | Port | +|---------|-------------|------| +| **db** | PostgreSQL 16 | 5432 | +| **crypto** | AES-256-GCM encryption gRPC service | 50051 | +| **backend** | Fastify API | 8080 | +| **dashboard** | Next.js dashboard | 3000 | +| **caddy** | Reverse proxy with auto-HTTPS | 80, 443 | + +Database migrations run automatically on backend startup. + +### Verify the deployment + +```bash +# Check all services are healthy +docker compose ps + +# Check backend logs +docker compose logs backend + +# Test the API +curl https://api.example.com/health +``` + +## Step 5: Connect the CLI + +Install the Keyway CLI and point it at your instance: + +```bash +# Install +brew install keywaysh/tap/keyway + +# Set your API URL +export KEYWAY_API_URL=https://api.example.com + +# Login +keyway login + +# Use it +keyway init +keyway push +keyway pull +``` + +Add `KEYWAY_API_URL` to your shell profile (`~/.bashrc`, `~/.zshrc`) to persist it. + +--- + +## Architecture + +``` + ┌──────────┐ + │ Caddy │ :80, :443 + │(reverse │ Auto HTTPS via Let's Encrypt + │ proxy) │ + └────┬─────┘ + │ + ┌─────────┴─────────┐ + │ │ + ┌─────▼──────┐ ┌─────▼──────┐ + │ Dashboard │ │ Backend │ + │ (Next.js) │ │ (Fastify) │ + │ :3000 │ │ :8080 │ + └────────────┘ └──┬───┬─────┘ + │ │ + ┌────▼┐ ┌▼────────┐ + │ DB │ │ Crypto │ + │(PG) │ │ (gRPC) │ + │:5432│ │ :50051 │ + └─────┘ └──────────┘ +``` + +- **Caddy** handles TLS termination and reverse proxying +- **Backend** runs database migrations on startup, handles all API logic +- **Crypto** is an isolated gRPC service that holds the encryption key +- **PostgreSQL** stores users, vaults, encrypted secrets, and audit logs + +--- + +## Optional Services + +All third-party services are optional. Without them, Keyway works with full core functionality. Leave their env vars empty to disable them. + +### Billing (Stripe) + +Controls plan limits (free vs pro vs team). **Disabled by default** — all users get unlimited access. + +```bash +BILLING_ENABLED=true +STRIPE_SECRET_KEY=sk_live_... +STRIPE_WEBHOOK_SECRET=whsec_... +``` + +### Email (Resend) + +Sends welcome emails and security alerts (new device login, impossible travel). + +```bash +RESEND_API_KEY=re_... +EMAIL_FROM_ADDRESS=Keyway +EMAIL_FROM_NAME=hello@example.com +``` + +Without this, no emails are sent. Users can still authenticate and use all features. + +### Analytics (PostHog) + +Usage metrics. Never tracks secret values. + +```bash +# Server-side (backend) +POSTHOG_API_KEY=phc_... +POSTHOG_HOST=https://app.posthog.com + +# Client-side (dashboard build args) +NEXT_PUBLIC_POSTHOG_KEY=phc_... +NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com +``` + +### Error Tracking (Sentry) + +```bash +SENTRY_DSN=https://...@sentry.io/... +NEXT_PUBLIC_SENTRY_DSN=https://...@sentry.io/... +``` + +### Live Chat (Crisp) + +```bash +NEXT_PUBLIC_CRISP_WEBSITE_ID=abc123 +``` + +### IP Geolocation (ipinfo.io) + +Enables security features like impossible travel detection and new location alerts. + +```bash +IPINFO_TOKEN=abc123 +``` + +### Provider Integrations (Vercel/Netlify) + +Enables syncing secrets to deployment platforms. + +```bash +VERCEL_CLIENT_ID=... +VERCEL_CLIENT_SECRET=... +NETLIFY_CLIENT_ID=... +NETLIFY_CLIENT_SECRET=... +``` + +--- + +## GitHub Enterprise + +To use GitHub Enterprise Server instead of github.com: + +```bash +GITHUB_URL=https://github.example.com +GITHUB_BASE_URL=https://github.example.com/api/v3 +``` + +The CLI also supports this: +```bash +export KEYWAY_GITHUB_URL=https://github.example.com +export KEYWAY_GITHUB_API_URL=https://github.example.com/api/v3 +``` + +--- + +## GitHub Action + +Use the Keyway GitHub Action in CI workflows with your self-hosted instance: + +```yaml +- uses: keywaysh/keyway-action@v1 + with: + api-url: https://api.example.com + token: ${{ secrets.KEYWAY_TOKEN }} +``` + +## MCP Server + +The MCP server for AI assistants works with self-hosted instances: + +```bash +KEYWAY_API_URL=https://api.example.com npx @keywaysh/mcp +``` + +--- + +## Local Development + +For local development with `keyway.local` domains and mkcert certificates: + +```bash +# Run setup (configures /etc/hosts and generates certs) +make setup + +# Use the local Caddyfile +CADDYFILE=./Caddyfile docker compose up --build +``` + +This gives you: +- `https://app.keyway.local` (dashboard) +- `https://api.keyway.local` (API) + +Alternatively, run services directly without Docker: +```bash +make dev # Starts crypto, backend, and dashboard +make dev-backend # Backend only +make dev-dashboard # Dashboard only +``` + +--- + +## Upgrading + +```bash +git pull +docker compose up --build -d +``` + +Database migrations run automatically on backend startup. + +--- + +## Troubleshooting + +### Backend won't start +Check that the database is healthy and the encryption key is set: +```bash +docker compose logs db +docker compose logs crypto +docker compose logs backend +``` + +### TLS certificate issues +Caddy needs ports 80 and 443 open for the ACME challenge: +```bash +docker compose logs caddy +``` + +For local development, use `CADDYFILE=./Caddyfile` with mkcert certificates instead. + +### Cookie/logout issues on custom domain +The dashboard derives the cookie domain from the browser hostname. Verify that the API and dashboard share a common parent domain. + +### CLI can't connect +Ensure `KEYWAY_API_URL` is set and the API is reachable: +```bash +export KEYWAY_API_URL=https://api.example.com +curl $KEYWAY_API_URL/health +keyway login +``` + +### GitHub App callback fails +Verify the callback URL in your GitHub App settings matches exactly: +``` +https://api.example.com/v1/auth/callback +``` diff --git a/packages/docs/sidebars.ts b/packages/docs/sidebars.ts index fcc9c63..f683187 100644 --- a/packages/docs/sidebars.ts +++ b/packages/docs/sidebars.ts @@ -12,6 +12,7 @@ const sidebars: SidebarsConfig = { 'security', 'organizations', 'integrations', + 'self-hosting', ], }; From 008047830e1b691b05c3d4bb5387738e9b6ff8f5 Mon Sep 17 00:00:00 2001 From: Nicolas Ritouet Date: Mon, 16 Feb 2026 01:34:03 +0100 Subject: [PATCH 3/5] docs: address CodeRabbit review comments - Add language identifiers to fenced code blocks (MD040) - Fix EMAIL_FROM_NAME/EMAIL_FROM_ADDRESS example values - Capitalize GitHub.com per official branding Co-Authored-By: Claude Opus 4.6 --- packages/docs/docs/self-hosting.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/docs/docs/self-hosting.md b/packages/docs/docs/self-hosting.md index 9097621..47d1a80 100644 --- a/packages/docs/docs/self-hosting.md +++ b/packages/docs/docs/self-hosting.md @@ -192,7 +192,7 @@ Add `KEYWAY_API_URL` to your shell profile (`~/.bashrc`, `~/.zshrc`) to persist ## Architecture -``` +```text ┌──────────┐ │ Caddy │ :80, :443 │(reverse │ Auto HTTPS via Let's Encrypt @@ -241,8 +241,8 @@ Sends welcome emails and security alerts (new device login, impossible travel). ```bash RESEND_API_KEY=re_... -EMAIL_FROM_ADDRESS=Keyway -EMAIL_FROM_NAME=hello@example.com +EMAIL_FROM_ADDRESS=hello@mail.example.com +EMAIL_FROM_NAME=Keyway ``` Without this, no emails are sent. Users can still authenticate and use all features. @@ -297,7 +297,7 @@ NETLIFY_CLIENT_SECRET=... ## GitHub Enterprise -To use GitHub Enterprise Server instead of github.com: +To use GitHub Enterprise Server instead of GitHub.com: ```bash GITHUB_URL=https://github.example.com @@ -400,6 +400,6 @@ keyway login ### GitHub App callback fails Verify the callback URL in your GitHub App settings matches exactly: -``` +```text https://api.example.com/v1/auth/callback ``` From dade58f546511249dbe6f0339b485f87587b03bb Mon Sep 17 00:00:00 2001 From: Nicolas Ritouet Date: Thu, 5 Mar 2026 21:07:03 +0100 Subject: [PATCH 4/5] fix(cli): map .env.local to development environment .env.local (Next.js/Vite convention) was mapped to "local" which doesn't exist as a vault environment, causing push to fail. Add alias mapping for common env file suffixes (.local, .dev, .prod, .stage, .*.local) to standard vault environments. Co-Authored-By: Claude Opus 4.6 --- packages/cli/internal/env/discover.go | 20 +++++++++++++++++++- packages/cli/internal/env/discover_test.go | 17 ++++++++++++----- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/cli/internal/env/discover.go b/packages/cli/internal/env/discover.go index 07e254b..161a8dd 100644 --- a/packages/cli/internal/env/discover.go +++ b/packages/cli/internal/env/discover.go @@ -40,9 +40,23 @@ func Discover() []Candidate { return candidates } +// envAliases maps common framework-specific env file suffixes to standard +// vault environment names (e.g. Next.js/Vite ".env.local" → "development"). +var envAliases = map[string]string{ + "local": "development", + "dev": "development", + "prod": "production", + "stage": "staging", + "development.local": "development", + "production.local": "production", + "staging.local": "staging", + "test.local": "development", +} + // DeriveEnvFromFile derives the environment name from a filename. // Examples: // - ".env" -> "development" +// - ".env.local" -> "development" // - ".env.production" -> "production" // - ".env.staging" -> "staging" func DeriveEnvFromFile(file string) string { @@ -51,7 +65,11 @@ func DeriveEnvFromFile(file string) string { return "development" } if strings.HasPrefix(base, ".env.") { - return strings.TrimPrefix(base, ".env.") + suffix := strings.TrimPrefix(base, ".env.") + if mapped, ok := envAliases[suffix]; ok { + return mapped + } + return suffix } return "development" } diff --git a/packages/cli/internal/env/discover_test.go b/packages/cli/internal/env/discover_test.go index dec411e..a868726 100644 --- a/packages/cli/internal/env/discover_test.go +++ b/packages/cli/internal/env/discover_test.go @@ -12,7 +12,7 @@ func TestDeriveEnvFromFile(t *testing.T) { expected string }{ {".env", "development"}, - {".env.local", "local"}, + {".env.local", "development"}, {".env.development", "development"}, {".env.production", "production"}, {".env.staging", "staging"}, @@ -121,8 +121,8 @@ func TestDiscover_IncludesEnvLocal(t *testing.T) { for _, c := range candidates { if c.File == ".env.local" { foundEnvLocal = true - if c.Env != "local" { - t.Errorf(".env.local should map to 'local', got %q", c.Env) + if c.Env != "development" { + t.Errorf(".env.local should map to 'development', got %q", c.Env) } } } @@ -215,13 +215,20 @@ func TestDeriveEnvFromFile_EdgeCases(t *testing.T) { }{ // Edge cases {".env.", ""}, // Empty suffix - {".env.dev.local", "dev.local"}, // Multiple dots {"config", "development"}, // Not an env file format {"", "development"}, // Empty string {".env.PRODUCTION", "PRODUCTION"}, // Case preserved {"./path/to/.env.staging", "staging"}, // Relative path - {"/absolute/path/.env.prod", "prod"}, // Absolute path {".env.development.backup", "development.backup"}, // Multiple parts + // Alias mappings + {".env.local", "development"}, // Next.js/Vite local + {".env.dev", "development"}, // Common shorthand + {"/absolute/path/.env.prod", "production"},// Common shorthand + {".env.stage", "staging"}, // Common shorthand + {".env.development.local", "development"}, // Next.js local override + {".env.production.local", "production"}, // Next.js local override + {".env.staging.local", "staging"}, // Next.js local override + {".env.test.local", "development"}, // Next.js local override } for _, tt := range tests { From 2c5624c8292622b40851e5ce1a14d69ecb64ff88 Mon Sep 17 00:00:00 2001 From: Nicolas Ritouet Date: Thu, 5 Mar 2026 21:48:45 +0100 Subject: [PATCH 5/5] fix(cli): address CodeRabbit review comments - Add missing shorthand *.local aliases (dev.local, prod.local, stage.local) - Export NormalizeEnvName in env package and reuse it in diff.go to centralize env name normalization - Add stg alias for staging - Normalize env names to lowercase Co-Authored-By: Claude Opus 4.6 --- packages/cli/internal/cmd/diff.go | 20 +++++-------------- packages/cli/internal/env/discover.go | 23 ++++++++++++++++------ packages/cli/internal/env/discover_test.go | 6 +++++- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/packages/cli/internal/cmd/diff.go b/packages/cli/internal/cmd/diff.go index 9ff59e8..b3f7ffc 100644 --- a/packages/cli/internal/cmd/diff.go +++ b/packages/cli/internal/cmd/diff.go @@ -8,7 +8,7 @@ import ( "github.com/fatih/color" "github.com/keywaysh/cli/internal/analytics" - "github.com/keywaysh/cli/internal/env" + envpkg "github.com/keywaysh/cli/internal/env" "github.com/keywaysh/cli/internal/ui" "github.com/spf13/cobra" ) @@ -186,14 +186,14 @@ func runDiffWithDeps(opts DiffOptions, deps *Dependencies) error { if err != nil { pullErr1 = err } else { - secrets1 = env.Parse(resp1.Content) + secrets1 = envpkg.Parse(resp1.Content) } resp2, err := client.PullSecrets(ctx, repo, env2) if err != nil { pullErr2 = err } else { - secrets2 = env.Parse(resp2.Content) + secrets2 = envpkg.Parse(resp2.Content) } return nil @@ -241,18 +241,8 @@ func runDiffWithDeps(opts DiffOptions, deps *Dependencies) error { return nil } -func normalizeEnvName(env string) string { - env = strings.ToLower(strings.TrimSpace(env)) - switch env { - case "prod": - return "production" - case "dev": - return "development" - case "stg": - return "staging" - default: - return env - } +func normalizeEnvName(name string) string { + return envpkg.NormalizeEnvName(name) } func compareSecrets(env1, env2 string, secrets1, secrets2 map[string]string, includeValues bool) *DiffResult { diff --git a/packages/cli/internal/env/discover.go b/packages/cli/internal/env/discover.go index 161a8dd..58aa36f 100644 --- a/packages/cli/internal/env/discover.go +++ b/packages/cli/internal/env/discover.go @@ -40,17 +40,31 @@ func Discover() []Candidate { return candidates } -// envAliases maps common framework-specific env file suffixes to standard -// vault environment names (e.g. Next.js/Vite ".env.local" → "development"). +// envAliases maps common framework-specific env file suffixes and shorthand +// names to standard vault environment names. var envAliases = map[string]string{ "local": "development", "dev": "development", "prod": "production", "stage": "staging", + "stg": "staging", "development.local": "development", "production.local": "production", "staging.local": "staging", "test.local": "development", + "dev.local": "development", + "prod.local": "production", + "stage.local": "staging", +} + +// NormalizeEnvName maps shorthand or framework-specific environment names +// to their canonical vault environment names. +func NormalizeEnvName(name string) string { + name = strings.ToLower(strings.TrimSpace(name)) + if mapped, ok := envAliases[name]; ok { + return mapped + } + return name } // DeriveEnvFromFile derives the environment name from a filename. @@ -66,10 +80,7 @@ func DeriveEnvFromFile(file string) string { } if strings.HasPrefix(base, ".env.") { suffix := strings.TrimPrefix(base, ".env.") - if mapped, ok := envAliases[suffix]; ok { - return mapped - } - return suffix + return NormalizeEnvName(suffix) } return "development" } diff --git a/packages/cli/internal/env/discover_test.go b/packages/cli/internal/env/discover_test.go index a868726..4d6f4ea 100644 --- a/packages/cli/internal/env/discover_test.go +++ b/packages/cli/internal/env/discover_test.go @@ -217,7 +217,7 @@ func TestDeriveEnvFromFile_EdgeCases(t *testing.T) { {".env.", ""}, // Empty suffix {"config", "development"}, // Not an env file format {"", "development"}, // Empty string - {".env.PRODUCTION", "PRODUCTION"}, // Case preserved + {".env.PRODUCTION", "production"}, // Case normalized {"./path/to/.env.staging", "staging"}, // Relative path {".env.development.backup", "development.backup"}, // Multiple parts // Alias mappings @@ -228,7 +228,11 @@ func TestDeriveEnvFromFile_EdgeCases(t *testing.T) { {".env.development.local", "development"}, // Next.js local override {".env.production.local", "production"}, // Next.js local override {".env.staging.local", "staging"}, // Next.js local override + {".env.dev.local", "development"}, // Shorthand + local + {".env.prod.local", "production"}, // Shorthand + local + {".env.stage.local", "staging"}, // Shorthand + local {".env.test.local", "development"}, // Next.js local override + {".env.stg", "staging"}, // Common shorthand } for _, tt := range tests {