Skip to content

Helpers: env() & prompt()

CT Dev provides two helper globals for parameterizing dev.ctenv() for environment variables and prompt() for interactive input with caching.

Read an environment variable from the merged .env file + system environment.

function env(name: string): string
function env<T extends number>(name: string, defaultValue: T): T | string
ParameterTypeRequiredDescription
namestringyesEnvironment variable name.
defaultValueTnoFallback value if the variable is not set. Also controls type coercion.
ScenarioReturn
Variable exists, no defaultstring — raw value
Variable exists, default is number (int)number — parsed as integer, falls back to string on parse error
Variable exists, default is number (float)number — parsed as float, falls back to string on parse error
Variable not set, no default"" — empty string
Variable not set, default provideddefaultValue — returned as-is

When a defaultValue is provided and the variable exists, the string value is coerced to match the type of the default:

Default typeCoercion
number (integer, e.g. 8080)parseInt — returns number if valid, otherwise returns raw string
number (float, e.g. 0.5)parseFloat — returns number if valid, otherwise returns raw string
anything elseNo coercion — raw string returned

This means the default value serves dual purpose: fallback and type hint.

Variables are loaded from two sources, merged in order:

  1. .env file — loaded from the project directory (or custom path via --env-file).
  2. System environmentos.Environ().

System environment variables override .env file values.

.env
API_PORT=3000
DB_HOST=localhost
# system
export API_PORT=8080
env("API_PORT") // → "8080" (system wins)
env("DB_HOST") // → "localhost" (from .env)
env("MISSING") // → ""
// Simple string read
const region = env("AWS_REGION");
// With numeric default (type coercion)
const apiPort = env("API_PORT", 8080); // number if set, 8080 if not
const dbPort = env("DB_PORT", 5432); // number if set, 5432 if not
// With string default
const logLevel = env("LOG_LEVEL", "info"); // "debug" if set, "info" if not
// Use in config
config({
namespace: env("NAMESPACE", "default"),
values: {
apiPort: env("API_PORT", 3000),
},
});
FlagEffect
(default)Loads .env from project root + system env
--env-file .env.devLoads .env.dev instead of .env
--env-file ""Skips .env loading entirely (system env only)

Interactively prompt the user for input. Answers are cached on disk so subsequent runs reuse the previous answer without asking again.

function prompt(question: string): string
ParameterTypeRequiredDescription
questionstringyesThe question text displayed to the user.

string — the user’s trimmed answer (leading/trailing whitespace removed).

Answers are persisted to ~/.ct/prompt_cache/<project-hash>.json, keyed by the exact question string.

RunBehavior
First runPrints question: and waits for stdin input. Saves answer to cache.
Subsequent runsPrints question [cached: answer] and returns immediately.

The cache file is a simple JSON map:

{
"Which username do you want to use?": "john",
"Target cluster?": "staging"
}

To reset a cached answer, delete or edit ~/.ct/prompt_cache/<hash>.json.

The project hash is derived from the absolute path of the project directory (SHA-256, first 8 bytes, hex-encoded).

// Per-developer namespace
const USERNAME = prompt("Which username do you want to use?");
const NAMESPACE = `myapp-dev-${USERNAME}`;
config({
namespace: NAMESPACE,
});
// Dynamic target selection
const CLUSTER = prompt("Target cluster?");
config({
values: {
cluster: CLUSTER,
},
});
$ ct dev
Which username do you want to use?: john
Target cluster?: staging
...
$ ct dev
Which username do you want to use? [cached: john]
Target cluster? [cached: staging]
...

A common pattern is to use env() for CI/automation and prompt() for local development:

const USERNAME = env("DEV_USER") || prompt("Your username?");
const NAMESPACE = `myapp-dev-${USERNAME}`;
const API_PORT = env("API_PORT", 8080);
const DB_PORT = env("DB_PORT", 5432);
config({
namespace: NAMESPACE,
values: {
dev: true,
apiPort: API_PORT,
dbPort: DB_PORT,
},
});

In CI, set DEV_USER=ci to skip the prompt. Locally, the prompt fires and caches the answer.