defineEnv & defineEnvResolver

defineEnvResolver() declares how environment variables are fetched from a backing store, and defineEnv() declares which variables a task wants from that resolver.

Model

pok's environment system has two parts:

  1. Resolver: the backing implementation that fetches variables from systems such as 1Password or AWS Secrets Manager.
  2. Env: the selection layer that tells a task which variables it needs.

Imports

import { defineEnv, defineEnvResolver } from '@pokit/core';

defineEnvResolver

Creates a resolver that fetches environment variables based on context.

Signature

function defineEnvResolver<TContext, TAvailableVars extends string>(config: {
  requiredContext: z.ZodType<TContext>;
  availableVars: readonly TAvailableVars[];
  resolve: (keys: string[], context: TContext) => Promise<Record<string, string>>;
  write?: (values: Record<string, string>, context: TContext) => Promise<void>;
}): TypedEnvResolver<TAvailableVars>;

Configuration

PropertyTypeDescription
requiredContextz.ZodTypeZod schema for context this resolver needs
availableVarsreadonly string[]List of variable names this resolver can provide
resolveFunctionAsync function to fetch requested variables
writeFunctionOptional function to persist variables

Example

import { z } from 'zod';
import { defineEnvResolver } from '@pokit/core';

const opResolver = defineEnvResolver({
  // This resolver needs an 'env' field in the command context
  requiredContext: z.object({
    env: z.enum(['dev', 'staging', 'prod']),
  }),

  // Variables this resolver can provide
  availableVars: ['DATABASE_URL', 'REDIS_URL', 'API_KEY', 'STRIPE_SECRET_KEY'] as const, // Use 'as const' for type inference

  // Fetch the requested keys
  resolve: async (keys, ctx) => {
    const vault = getVaultForEnv(ctx.env);
    const secrets: Record<string, string> = {};

    for (const key of keys) {
      secrets[key] = await vault.get(key);
    }

    return secrets;
  },

  // Optional: write secrets back
  write: async (values, ctx) => {
    const vault = getVaultForEnv(ctx.env);
    for (const [key, value] of Object.entries(values)) {
      await vault.set(key, value);
    }
  },
});

defineEnv

Creates an environment that selects specific variables from a resolver.

Signature

function defineEnv<TResolver, TVars extends TResolver['availableVars'][number]>(config: {
  resolver: TResolver;
  vars: readonly TVars[];
}): Env<InferResolverContext<TResolver>, TVars>;

Example

import { defineEnv } from '@pokit/core';

// Select only database variables
const dbEnv = defineEnv({
  resolver: opResolver,
  vars: ['DATABASE_URL', 'REDIS_URL'],
});

// Select only API keys
const apiEnv = defineEnv({
  resolver: opResolver,
  vars: ['API_KEY', 'STRIPE_SECRET_KEY'],
});

Using in Tasks

import { defineTask } from '@pokit/core';

// Task with single env
const migrateTask = defineTask({
  label: 'Run migrations',
  env: dbEnv,
  exec: (ctx) => {
    // ctx.envs.DATABASE_URL is available as string
    // ctx.envs.REDIS_URL is available as string
    return 'prisma migrate deploy';
  },
});

// Task with multiple envs
const deployTask = defineTask({
  label: 'Deploy',
  env: [dbEnv, apiEnv], // All vars merged
  run: async (r, ctx) => {
    // ctx.envs.DATABASE_URL
    // ctx.envs.REDIS_URL
    // ctx.envs.API_KEY
    // ctx.envs.STRIPE_SECRET_KEY
    await r.exec('deploy');
  },
});

Type Inference

Infer Resolver Context

import type { InferResolverContext } from '@pokit/core';

type Ctx = InferResolverContext<typeof opResolver>;
// { env: 'dev' | 'staging' | 'prod' }

Infer Env Vars

import type { InferEnvVars } from '@pokit/core';

type Vars = InferEnvVars<typeof dbEnv>;
// { DATABASE_URL: string; REDIS_URL: string }

Infer Env Context

import type { InferEnvContext } from '@pokit/core';

type Ctx = InferEnvContext<typeof dbEnv>;
// { env: 'dev' | 'staging' | 'prod' }

Composite Resolvers

Combine multiple resolvers into one:

import { defineCompositeResolver } from '@pokit/core';

const compositeResolver = defineCompositeResolver({
  requiredContext: z.object({
    env: z.enum(['dev', 'staging', 'prod']),
    region: z.enum(['us', 'eu']),
  }),
  resolvers: [
    {
      resolver: opResolver,
      mapContext: (ctx) => ({ env: ctx.env }),
    },
    {
      resolver: awsResolver,
      mapContext: (ctx) => ({ region: ctx.region }),
    },
  ],
});

Writing Environment Variables

Resolvers can optionally implement a write method for persisting values:

// Resolver with write capability
const secretsResolver = defineEnvResolver({
  requiredContext: z.object({ env: z.enum(['dev', 'prod']) }),
  availableVars: ['NEW_API_KEY'] as const,
  resolve: async (keys, ctx) => {
    /* ... */
  },
  write: async (values, ctx) => {
    // Persist values to secret store
    await secretStore.write(ctx.env, values);
  },
});

// Env for writing
const writableEnv = defineEnv({
  resolver: secretsResolver,
  vars: ['NEW_API_KEY'],
});

// Task that writes
const bootstrapTask = defineTask({
  label: 'Bootstrap',
  envWriter: writableEnv, // Enables ctx.writeEnvs
  run: async (r, ctx) => {
    const key = generateApiKey();
    await ctx.writeEnvs({ NEW_API_KEY: key });
  },
});

Context Requirements

Tasks inherit context from their parent command. The type system ensures commands provide the context that tasks require:

// Task requires { env: 'dev' | 'prod' }
const task = defineTask({
  env: dbEnv, // dbEnv's resolver requires { env }
  exec: 'migrate',
});

// Command must provide matching context
const command = defineCommand({
  context: {
    env: {
      from: 'flag',
      schema: z.enum(['dev', 'prod']), // Satisfies requirement
    },
  },
  run: async (r) => {
    await r.run(task); // OK
  },
});

Helper Functions

getEnvKeys

Get the list of variable keys from an env:

import { getEnvKeys } from '@pokit/core';

const keys = getEnvKeys(dbEnv);
// ['DATABASE_URL', 'REDIS_URL']