Skip to content

Project Mode

Garret Premo edited this page May 9, 2026 · 5 revisions

Project Mode

By default, apijack stores all configuration globally at ~/.<cliName>/. Project mode lets you scope config, routines, and extensions to a specific project directory — checked into git alongside your code. This makes it easy to share routines with teammates, run consistent setups in CI/CD, and add project-specific commands or authentication.

Global vs Project Mode

Global Mode Project Mode
Config ~/.<cliName>/config.json .apijack/config.json
Routines ~/.<cliName>/routines/ .apijack/routines/
Generated files ~/.<cliName>/generated/ configurable via generatedDir
Activated by default .apijack.json in project root

Project mode is activated automatically when apijack finds a .apijack.json file by walking up from the current working directory. Global mode is the fallback when no such file exists.

Setting Up Project Mode

Create a .apijack.json file in your project root. The minimum required config just needs to be valid JSON:

{}

A more typical setup:

{
  "name": "my-api",
  "specUrl": "https://api.example.com/v3/api-docs",
  "generatedDir": "src/generated"
}

Config File Format

All fields in .apijack.json are optional:

Field Type Description
name string Name of the CLI / project. When the shared apijack binary runs in project mode, this overrides the programName shown in --help and setup hints.
description string Description shown in --help output when running the shared apijack binary in project mode. Falls back to the framework default if omitted.
version string Version shown in --version output when running the shared apijack binary in project mode. Falls back to the installed @apijack/core version if omitted.
specUrl string URL to the OpenAPI spec used by generate
generatedDir string Path where generated files are written (default: ~/.<cliName>/generated/)
auth string Auth strategy identifier (used to select from registered strategies)
allowedCidrs string[] CIDR blocks permitted to call the API (for network-restricted environments)

Example with all fields:

{
  "name": "my-api",
  "description": "My API CLI",
  "version": "1.0.0",
  "specUrl": "https://api.example.com/v3/api-docs",
  "generatedDir": "src/generated",
  "auth": "bearer",
  "allowedCidrs": ["10.0.0.0/8", "192.168.1.0/24"]
}

Project-Local Config

When project mode is active, environment credentials are stored in .apijack/config.json inside your project directory rather than in your home directory. This file contains secrets, so add .apijack/ to .gitignore:

# .gitignore
.apijack/

The .apijack/ directory is also where extension files live (see below). Commit the extension files but not the config file.

Project-Local Routines

Place routine YAML files in .apijack/routines/. See Writing Routines for the full routine format.

my-project/
  .apijack.json
  .apijack/
    config.json
    routines/
      deploy.yaml
      seed-database.yaml

Extension Points

apijack loads three kinds of project-local extensions from the .apijack/ directory at startup.

Custom Auth — .apijack/auth.ts

Export an AuthStrategy as the default export to override the auth strategy passed to createCli. Useful when your project uses session-based auth, OAuth, or any mechanism not covered by the built-in strategies.

// .apijack/auth.ts
import type { AuthStrategy } from "@apijack/core";

const myAuth: AuthStrategy = {
  async getHeaders() {
    const token = await fetchTokenFromVault();
    return { Authorization: `Bearer ${token}` };
  },
};

export default myAuth;

Custom Commands — .apijack/commands/*.ts

Each .ts file in .apijack/commands/ can export a CommandRegistrar function as its default export. A registrar receives the root Commander program and a CliContext, and can attach any subcommands.

The command name defaults to the filename (without .ts) unless the module also exports a name string.

// .apijack/commands/deploy.ts
import type { CommandRegistrar } from "@apijack/core";

export const name = "deploy";

const registrar: CommandRegistrar = (program, ctx) => {
  program
    .command("deploy")
    .description("Deploy the current build")
    .action(async () => {
      // custom logic using ctx.client, ctx.config, etc.
    });
};

export default registrar;

Opt-in auth resolution

By default, ctx.session is lazily resolved — it stays null until a generated API command triggers resolution. Custom commands that read ctx.session directly need to opt in so the framework resolves the session before the action runs.

// .apijack/commands/whoami.ts
import type { CommandRegistrar, AuthedCliContext } from "@apijack/core";

export const name = "whoami";
export const requiresAuth = true;

const registrar: CommandRegistrar<true> = (program, ctx: AuthedCliContext) => {
  program.command("whoami").action(() => {
    // ctx.session is guaranteed non-null here
    console.log(ctx.session.headers);
  });
};

export default registrar;

The CommandRegistrar<true> generic narrows ctx to AuthedCliContext (non-null session). Runtime enforcement is independent of the type annotation — setting export const requiresAuth = true resolves the session regardless of how the registrar is typed.

Preview modes (--dry-run, -o curl, -o routine-step, --help) skip the auth hook so command discovery stays offline. -o curl-with-creds still resolves, since the output includes credentials.

Project-wide default via .apijack/settings.json

To flip the default for every custom command and dispatcher in the project, create .apijack/settings.json:

{
  "customCommands": {
    "defaults": {
      "requiresAuth": true
    }
  }
}

A module-level export const requiresAuth = false still wins over this default.

Persisting session mutations — ctx.saveSession()

If a custom command mutates ctx.session (e.g. after rotating a token), call ctx.saveSession() to write the new value to disk. Setting ctx.session = null and calling ctx.saveSession() deletes the on-disk session file — useful for logout flows.

// .apijack/commands/logout.ts
export const name = "logout";
export const requiresAuth = true;

export default (program, ctx) => {
  program.command("logout").action(async () => {
    ctx.session = null;
    await ctx.saveSession();
  });
};

ctx.resolveSession() is also available for on-demand resolution from code paths that didn't set requiresAuth.

Custom Dispatchers — .apijack/dispatchers/*.ts

Each .ts file in .apijack/dispatchers/ can export a DispatcherHandler as its default export. Dispatchers intercept command execution by name, letting you override or wrap the default behavior for specific operations.

The dispatcher is keyed by the filename (without .ts) unless the module exports a name string.

// .apijack/dispatchers/resources-create.ts
import type { DispatcherHandler } from "@apijack/core";

export const name = "resources-create";

const handler: DispatcherHandler = async (args, positionalArgs, ctx) => {
  // Pre-processing
  console.log("Creating resource with args:", args);

  // Delegate to the default dispatcher or implement directly
  return ctx.dispatch("resources-create", args, positionalArgs);
};

export default handler;

Dispatchers use the same requiresAuth opt-in as custom commands. Set export const requiresAuth = true (or rely on .apijack/settings.json) to have the framework resolve ctx.session before the handler runs, and type the handler as DispatcherHandler<true> to narrow ctx to AuthedCliContext.

Custom Resolvers — .apijack/resolvers/*.ts

Each .ts file in .apijack/resolvers/ can export a CustomResolver as its default export. Custom resolvers extend the set of $_*(...) functions available inside routines — useful when you need a project-specific data source (a secrets vault, an internal service, a computed test fixture) that the built-in resolvers don't cover.

The resolver is keyed by the filename (without .ts) unless the module exports a name. The name must start with an underscore — routine references always use $_name syntax.

// .apijack/resolvers/_vault.ts
import type { CustomResolver } from "@apijack/core";

export const name = "_vault";

const vault: CustomResolver = async (argsStr, helpers) => {
  // argsStr is the raw "(...)" contents. Use helpers.resolve to interpolate
  // any $ references the caller passed in.
  const key = helpers?.resolve(argsStr ?? "") ?? "";
  return await fetchSecret(key);
};

export default vault;

Used from a routine:

variables:
  api_token: "$_vault(petstore/prod/api-token)"

The helper receives:

  • argsStr — the raw string between ( and ) in the call (may be empty)
  • helpers.resolve(value) — a function that resolves $-references the caller may have passed in, using the current step's variable scope

Names that collide with a registered built-in resolver are rejected at load time with a warning. Files that fail to import are silently skipped — same as the other extension directories.

Files in any extension directory that fail to import are silently skipped.

When to Use Project Mode

Team projects — Commit .apijack/routines/ so everyone on the team has access to the same automation workflows without manual setup.

CI/CD pipelines — Check in .apijack.json and .apijack/routines/ so your pipeline can run <cli> routine run deploy with no additional configuration.

Project-specific commands — Use .apijack/commands/ to add commands that only make sense in this project's context without modifying the shared CLI package.

Project-specific auth — Use .apijack/auth.ts when different projects authenticate differently (e.g., one uses an API key, another uses short-lived tokens from a vault).

Clone this wiki locally