diff --git a/README.md b/README.md index b68718b..5e71b1c 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,39 @@ PRODUCTION_SITE_DIRECTORY= PRODUCTION_DATABASE_NAME= PRODUCTION_DATABASE_USERNAME= PRODUCTION_DATABASE_PASSWORD= + +# Custom environments (NEW!) +# You can now define any number of custom environments +# by following the pattern: {ENVIRONMENT_NAME}_{VARIABLE_TYPE} +# Example: A testing environment +TESTING_SERVER_IP= +TESTING_SERVER_ADDRESS= +TESTING_SERVER_USERNAME= +TESTING_SITE_DIRECTORY= +TESTING_DATABASE_NAME= +TESTING_DATABASE_USERNAME= +TESTING_DATABASE_PASSWORD= ``` +### Custom Environments + +As of this version, you can define custom environments by adding environment variables that follow the pattern `{ENVIRONMENT_NAME}_{VARIABLE_TYPE}`. The CLI will automatically detect and allow you to use these environments. + +**Example**: If you define `TESTING_SERVER_IP=...` variables, you can then run: +```sh +toward assets push -e testing +toward database pull -e testing +``` + +The CLI automatically detects environments based on the presence of these variable patterns: +- `{ENV}_SERVER_IP` +- `{ENV}_SERVER_ADDRESS` +- `{ENV}_SERVER_USERNAME` +- `{ENV}_SITE_DIRECTORY` +- `{ENV}_DATABASE_NAME` +- `{ENV}_DATABASE_USERNAME` +- `{ENV}_DATABASE_PASSWORD` + ## For Developers ### Requirements diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md new file mode 100644 index 0000000..e0b0b9f --- /dev/null +++ b/TESTING_GUIDE.md @@ -0,0 +1,73 @@ +# Dynamic Environment Support - Testing Guide + +This demonstrates how the enhanced Toward CLI now supports custom environments. + +## Setup Test Environment + +1. Create a `.toward` file with custom environment variables: + +```bash +# Standard environments (still supported) +STAGING_SERVER_IP=192.168.1.10 +STAGING_SERVER_ADDRESS=staging.example.com +STAGING_SERVER_USERNAME=forge +STAGING_SITE_DIRECTORY=/home/forge +STAGING_DATABASE_NAME=example_staging +STAGING_DATABASE_USERNAME=forge +STAGING_DATABASE_PASSWORD=password123 + +PRODUCTION_SERVER_IP=192.168.1.20 +PRODUCTION_SERVER_ADDRESS=production.example.com +PRODUCTION_SERVER_USERNAME=forge +PRODUCTION_SITE_DIRECTORY=/home/forge +PRODUCTION_DATABASE_NAME=example_production +PRODUCTION_DATABASE_USERNAME=forge +PRODUCTION_DATABASE_PASSWORD=password456 + +# Custom testing environment (NEW!) +TESTING_SERVER_IP=192.168.1.30 +TESTING_SERVER_ADDRESS=testing.example.com +TESTING_SERVER_USERNAME=forge +TESTING_SITE_DIRECTORY=/home/forge +TESTING_DATABASE_NAME=example_testing +TESTING_DATABASE_USERNAME=forge +TESTING_DATABASE_PASSWORD=password789 +``` + +## Expected Behavior + +After these changes, you can now run: + +```bash +# Standard environments (still work) +toward assets push -e staging +toward assets push -e production + +# Custom environment (NEW!) +toward assets push -e testing +``` + +## How It Works + +1. **Dynamic Detection**: The CLI scans environment variables for patterns like: + - `{ENV}_SERVER_IP` + - `{ENV}_SERVER_ADDRESS` + - `{ENV}_DATABASE_NAME` + - etc. + +2. **Environment Extraction**: From variable names like `TESTING_SERVER_IP`, it extracts "testing" as an environment name. + +3. **Validation**: The CLI validates that all required variables exist for an environment before allowing its use. + +## Help Text + +The help text now dynamically includes detected environments: + +**Before**: `` +**After**: `` + +## Backwards Compatibility + +- Existing `dev`, `staging`, `production` environments continue to work exactly as before +- No breaking changes to existing workflows +- Graceful fallback if no custom environments are defined \ No newline at end of file diff --git a/src/arguments/environment.ts b/src/arguments/environment.ts index dec8065..82711ad 100644 --- a/src/arguments/environment.ts +++ b/src/arguments/environment.ts @@ -1,13 +1,13 @@ import { Args } from "../../deps.ts"; import { Argument, Command, Environment } from "../../types.ts"; -import { environments } from "../constants.ts"; +import { getEnvironments } from "../constants.ts"; import ErrorMessage from "../libraries/messages/ErrorMessage.ts"; import getArgumentValue from "../utilities/getArgumentValue.ts"; /** The argument definition. */ const argument: Argument = { run: run, - description: `Which environment to perform the action on. <${environments.join(" | ")}>`, + description: `Which environment to perform the action on. <${getEnvironments().join(" | ")}>`, flags: ["environment", "env", "e"], }; @@ -24,7 +24,7 @@ function run(_command: Command, args: Args): Environment { new ErrorMessage("No environment has been entered.", true); } - if (!environments.includes(environment)) { + if (!getEnvironments().includes(environment)) { new ErrorMessage(`"${environment}" is not a valid environment in this context.`, true); } diff --git a/src/arguments/remoteEnvironment.ts b/src/arguments/remoteEnvironment.ts index 88237ea..e8f63ae 100644 --- a/src/arguments/remoteEnvironment.ts +++ b/src/arguments/remoteEnvironment.ts @@ -1,13 +1,13 @@ import { Args } from "../../deps.ts"; import { Argument, Command, Environment } from "../../types.ts"; -import { remoteEnvironments } from "../constants.ts"; +import { getRemoteEnvironments } from "../constants.ts"; import ErrorMessage from "../libraries/messages/ErrorMessage.ts"; import getArgumentValue from "../utilities/getArgumentValue.ts"; /** The argument definition. */ const argument: Argument = { run: run, - description: `Which environment to perform the action on. <${remoteEnvironments.join(" | ")}>`, + description: `Which environment to perform the action on. <${getRemoteEnvironments().join(" | ")}>`, flags: ["environment", "env", "e"], }; @@ -24,7 +24,7 @@ function run(_command: Command, args: Args): Environment { new ErrorMessage("No environment has been entered.", true); } - if (!remoteEnvironments.includes(environment)) { + if (!getRemoteEnvironments().includes(environment)) { new ErrorMessage(`"${environment}" is not a valid environment in this context.`, true); } diff --git a/src/constants.ts b/src/constants.ts index 6aef9ee..67ba61c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,6 @@ import { Environment } from "../types.ts"; import { exportEnvironmentVariables } from "./utilities/exportEnvironmentVariables.ts"; +import { detectEnvironments, detectLocalEnvironments, detectRemoteEnvironments } from "./utilities/detectEnvironments.ts"; import { version } from "./version.ts"; @@ -23,13 +24,27 @@ export const app = { contact: "dev@toward.studio", }; -export const environments: Environment[] = ["dev", "staging", "production"]; -export const localEnvironments: Environment[] = ["dev"]; -export const remoteEnvironments: Environment[] = ["staging", "production"]; - await exportEnvironmentVariables(".env"); await exportEnvironmentVariables(app.dotfile); +// Environment detection functions that return current state +export function getEnvironments(): Environment[] { + return detectEnvironments(); +} + +export function getLocalEnvironments(): Environment[] { + return detectLocalEnvironments(); +} + +export function getRemoteEnvironments(): Environment[] { + return detectRemoteEnvironments(); +} + +// For backwards compatibility, also export as constants +export const environments: Environment[] = detectEnvironments(); +export const localEnvironments: Environment[] = detectLocalEnvironments(); +export const remoteEnvironments: Environment[] = detectRemoteEnvironments(); + /** The project's environment variables represented as an object. */ export const env = { project: { diff --git a/src/utilities/detectEnvironments.ts b/src/utilities/detectEnvironments.ts new file mode 100644 index 0000000..1edc0c3 --- /dev/null +++ b/src/utilities/detectEnvironments.ts @@ -0,0 +1,70 @@ +/** + * Detects available environments by scanning environment variables for common patterns. + * + * This function looks for environment variables that follow the pattern: + * {ENVIRONMENT}_SERVER_IP, {ENVIRONMENT}_SERVER_ADDRESS, etc. + * + * @returns An array of detected environment names in lowercase + */ +export function detectEnvironments(): string[] { + const environmentPrefixes = new Set(); + + // Common environment variable suffixes that indicate an environment configuration + const environmentSuffixes = [ + '_SERVER_IP', + '_SERVER_ADDRESS', + '_SERVER_USERNAME', + '_SITE_DIRECTORY', + '_DATABASE_NAME', + '_DATABASE_USERNAME', + '_DATABASE_PASSWORD' + ]; + + // Scan all environment variables + for (const [key, value] of Object.entries(Deno.env.toObject())) { + // Skip empty values + if (!value || value.trim() === '') { + continue; + } + + // Check if this environment variable matches any of our expected patterns + for (const suffix of environmentSuffixes) { + if (key.endsWith(suffix)) { + // Extract the environment prefix (everything before the suffix) + const prefix = key.slice(0, -suffix.length); + if (prefix) { + environmentPrefixes.add(prefix.toLowerCase()); + } + break; + } + } + } + + // Convert to array and sort for consistent ordering + const environments = Array.from(environmentPrefixes).sort(); + + // Always include 'dev' as a local environment if not already present + if (!environments.includes('dev')) { + environments.unshift('dev'); + } + + return environments; +} + +/** + * Detects remote environments (excludes 'dev' which is local-only) + * + * @returns An array of detected remote environment names + */ +export function detectRemoteEnvironments(): string[] { + return detectEnvironments().filter(env => env !== 'dev'); +} + +/** + * Detects local environments (currently just 'dev') + * + * @returns An array of local environment names + */ +export function detectLocalEnvironments(): string[] { + return detectEnvironments().filter(env => env === 'dev'); +} \ No newline at end of file diff --git a/types.ts b/types.ts index dc9269b..d74e9f1 100644 --- a/types.ts +++ b/types.ts @@ -1,6 +1,6 @@ import { Args } from "./deps.ts"; -export type Environment = "dev" | "staging" | "production"; +export type Environment = string; export type Command = { /** The command's callable function. */