Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
@@ -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**: `<dev | staging | production>`
**After**: `<dev | production | staging | testing>`

## 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
6 changes: 3 additions & 3 deletions src/arguments/environment.ts
Original file line number Diff line number Diff line change
@@ -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"],
};

Expand All @@ -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);
}

Expand Down
6 changes: 3 additions & 3 deletions src/arguments/remoteEnvironment.ts
Original file line number Diff line number Diff line change
@@ -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"],
};

Expand All @@ -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);
}

Expand Down
23 changes: 19 additions & 4 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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: {
Expand Down
70 changes: 70 additions & 0 deletions src/utilities/detectEnvironments.ts
Original file line number Diff line number Diff line change
@@ -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<string>();

// 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');
}
2 changes: 1 addition & 1 deletion types.ts
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down