Parameters

Parameters are typed inputs to a build. You declare them as class fields — exactly like targets — and read the resolved value inside a target with this.<name>.value. The execution engine resolves every parameter before any target runs, so a build never starts with a missing or malformed input. The Concepts page introduces them in brief; this is the full reference.

import { Build, parameter, run, target } from "jsr:@zuke/core";

class Deploy extends Build {
  environment = parameter("Target environment")
    .options("dev", "staging", "production")
    .required();

  workers = parameter("Parallel upload workers").number().default(4);

  dryRun = parameter("Print actions without performing them").boolean();

  deploy = target().executes(() => {
    if (this.dryRun.value) console.log("(dry run)");
    console.log(
      `Deploying to ${this.environment.value} with ${this.workers.value} workers`,
    );
  });
}

await run(Deploy);
./zuke deploy --environment production --workers 8 --dry-run
ENVIRONMENT=staging ./zuke deploy        # value from the environment

Each fluent method returns a new parameter whose value type reflects the configuration — the builder narrows the type as you go, so value is exactly as strong as the declaration.

Declaring a parameter

parameter(description?) starts an optional string parameter. The optional description is shown in --help and --list. From there, configure it fluently:

MethodEffectvalue type
parameter("…")optional stringstring | undefined
.number()parse as a numbernumber | undefined
.boolean()a flag; defaults to falseboolean
.options("a", "b")restrict a string to choicesunchanged
.default(v)provide a defaultnon-optional (T)
.required()must be suppliednon-optional (T)
.env("NAME")override the env var nameunchanged
.secret()mark sensitive (masked everywhere)unchanged
.from(source)resolve from a secret managerunchanged
.array()a comma-separated / repeatable listT[]

Ordering matters: .number() and .boolean() come first (they change the kind and reset the default), .options() applies to strings, and .array() comes last so it composes with the kind and choices declared before it.

There is no .choices(...) — the method that restricts a string to a fixed set is .options(...). Three field names are also reserved: dryRun, confirm, and operatorToken collide with control keys Zuke's MCP server injects, so a parameter using one is rejected at discovery. Rename it.

Flags and environment variables

The command-line flag and environment variable are derived from the property name: the flag is kebab-case, the env var is SCREAMING_SNAKE_CASE. A camelCase field like targetEnv becomes --target-env and TARGET_ENV. Override just the environment variable with .env("NAME") — the flag is unaffected.

retries    = parameter().number();    // --retries      / RETRIES
targetEnv  = parameter();             // --target-env    / TARGET_ENV
apiKey     = parameter().env("CI_API_KEY"); // --api-key / CI_API_KEY

A flag accepts both --environment production and --environment=production. A boolean is set simply by naming the flag (--dry-run).

Typing the value

A bare parameter() is a string. .number() and .boolean() change the kind; .options() keeps it a string but restricts the accepted values.

// string (the default): value is `string | undefined`
region = parameter("Deploy region");

// number: parsed with Number(); empty or non-finite input is rejected.
workers = parameter("Upload workers").number();     // number | undefined

// boolean: a flag. Present (--verbose) → true; absent → false.
verbose = parameter("Verbose logging").boolean();   // boolean

// string restricted to a fixed set of choices.
level = parameter("Log level").options("debug", "info", "warn");
  • .number() parses with Number(); an empty string or a non-finite result ("eight", "") is rejected with a parse error.
  • .boolean() accepts true/1/yes and false/0/no (any casing) from a flag value or env var; anything else is rejected. As a flag with no value it is true, and it defaults to false when absent.
  • .options("a", "b") restricts the string to the listed choices; any other value is rejected naming the allowed set.

Lists with .array()

.array() turns a parameter into a list. On the command line, a comma separates values or the flag is repeated — the two are equivalent. Blank entries are dropped, and an unsupplied list defaults to [] (never undefined).

tags = parameter("Image tags").array();   // string[]
// deploy = target().executes(() => console.log(this.tags.value));
./zuke deploy --tags latest,canary          # ["latest", "canary"]
./zuke deploy --tags latest --tags canary   # same result
TAGS=latest,canary ./zuke deploy            # from the environment

Because .array() reuses the element parser of the parameter it is applied to, it composes with the kind and choices declared before it — every element is validated, not just the raw string:

// number[]: each entry parsed as a number; "1,x" is rejected.
workers = parameter("Worker ids").number().array();

// each element must be one of the choices; "api,nope" is rejected.
services = parameter("Services").options("api", "web", "worker").array();

.required() and .default()

By default a parameter is optional and its value is T | undefined. Two methods make it non-optional:

// Required: value is non-optional; the build fails before any target runs
// if neither a flag nor the env var (nor an interactive prompt) supplies it.
environment = parameter("Target environment").required();  // string

// Default: value is non-optional and falls back to the given value.
workers = parameter("Upload workers").number().default(4); // number

// A boolean is implicitly defaulted to false, so it is always non-optional.
dryRun = parameter("Dry run").boolean();                   // boolean
  • .required() demands a value. If none is supplied — and no default exists and no interactive prompt answers it — the build fails before any target runs, with a message naming the flag and env var.
  • .default(v) supplies a fallback, so value is always the declared type. A boolean is implicitly defaulted to false.

When a required parameter is missing and the build runs at an interactive terminal (a TTY, not CI), Zuke prompts for the value instead of failing. On CI or non-interactive runs it still errors, so automation stays deterministic.

.secret() and .from()

.secret() marks a value sensitive: its resolved value is redacted from all of Zuke's output — every banner, target status, summary, and error message — and, under GitHub Actions, Zuke emits an ::add-mask:: so the runner masks it in its own logs.

.from(source) resolves the value from a secret manager (1Password, Vault, a mounted file, …) when neither a flag nor an env var supplied one. It is a fallback provider, not an override, and is typically paired with .secret() so the fetched value is redacted.

import { execSecret } from "jsr:@zuke/core";

// .secret() masks the resolved value everywhere Zuke prints.
token = parameter("Deploy token").secret().required();

// .from(source) pulls the value from a secret manager when neither a flag
// nor an env var supplied one — a fallback provider, not an override.
registryToken = parameter("Registry token")
  .secret()
  .from(
    execSecret((s) => s.command("op").arg("read", "op://ci/registry/token")),
  );

The Secrets guide covers sources (execSecret, fileSecret), the exact redaction guarantee and its boundary, and the SecretError surface in full.

Resolution precedence

Every parameter is resolved before the first target executes, from — in order of precedence:

  1. a command-line flag — --environment production or --environment=production
  2. the environment variable — ENVIRONMENT (or the .env("NAME") override)
  3. the .from(...) secret source, if declared
  4. the declared default

Each level is consulted only when the ones above it produced nothing, so the source is invoked only when neither a flag nor an environment variable supplied a value. This is what keeps a build portable: in CI a value is usually injected as an environment variable (and the source is never touched); on a developer's machine the source pulls it from their secret manager — no code change either way. For a missing required value, an interactive prompt (at a TTY) is tried before the source.

Validation and errors

Resolution collects all parameter problems and reports them together, before any target runs — a missing required value, or a value that fails to parse (a non-number, a bad boolean, an off-list choice). The failure is a single block naming each offending flag:

Invalid or missing parameters:
  --environment is required (or set ENVIRONMENT).
  --workers: expected a number, got "eight"
  --level: expected one of debug, info, warn, got "trace"

The error class is ParameterError, exported for programmatic use. Reading .value before the build has resolved parameters (for example at construction time) also throws a ParameterError — read parameters inside a target body, not in a field initializer. For a .secret() parameter, the raw value is registered for redaction before it is parsed, so even a parse error on a malformed secret is masked rather than echoed.

Reading a value

Read a parameter inside a target via this.<name>.value. The value is fully typed by the declaration: a .required() or .default() parameter is non-optional, while a plain optional parameter is T | undefined.

deploy = target().executes(() => {
  const env = this.environment.value; // string  (.required())
  const n = this.workers.value;       // number  (.default(4))
  const region = this.region.value;   // string | undefined (plain optional)
});

Resolution lives in the execution engine, not the CLI, so a programmatic execute call resolves parameters too — pass raw values via params and/or rely on environment variables. See Concepts for the short version and Secrets for sensitive inputs.