Stainless Tools

A TypeScript library and CLI tool for managing Stainless config and generated SDKs. This tool helps you generate, monitor, and sync SDK repositories as you update your OpenAPI / Stainless config files.
Table of Contents
Features
- 🔄 Clone and monitor SDK repositories for changes
- 📄 Automatic OpenAPI specification file handling
- ⚙️ Flexible configuration using cosmiconfig
- 🔒 Support for both HTTPS and SSH git URLs
- 🚀 Easy-to-use CLI interface
- 📦 Can be used as a library in your own projects
- 🎯 Default configurations for easier usage
- 🔑 Integration with Stainless API for publishing changes
- 🔄 Lifecycle hooks for post-clone automation
About this project
This src
files were entirely built using Cursor. It took an entire day with careful prompting to cover various use-cases and write the appropriate tests and documentation. No human wrote any part of the src
code, although the README has been hand-polished.
It has only been tested on MacOS under Node 22 (although built for 18). Other platforms may not be supported.
Installation
You must have at least Node 18 installed to use this tool.
npm install stainless-tools -g
Prerequisites
Ensure you have:
- Write access to the SDK repository you want to generate
- A Stainless API key. This is the same API key used for automating updates via GitHub Action
- Have access to the generated SDK Repo as it will be checked out:
https://github.com/stainless-sdks/<project_name>-<language>
Environment Setup
Before using the tool, you need to set up your environment variables. You can do this in two ways:
- Create a
.env
file (or .env.override
if you auto-generate your .env
file) in your project root:
STAINLESS_API_KEY=your_api_key_here
- Or export them in your shell:
export STAINLESS_API_KEY=your_api_key_here
Configuration
The tool uses cosmiconfig for configuration management. You can define your configuration in any of these ways:
- A
stainless-tools
property in package.json
- A
.stainless-toolsrc
file in JSON or YAML format
- A
.stainless-toolsrc.json
, .stainless-toolsrc.yaml
, .stainless-toolsrc.yml
, .stainless-toolsrc.js
, or .stainless-toolsrc.cjs
file
- A
stainless-tools.config.js
or stainless-tools.config.cjs
CommonJS module
Configuration Schema
interface StainlessConfig {
stainlessSdkRepos: {
[key: string]: {
staging?: string;
prod?: string;
};
};
lifecycle?: {
[key: string]: {
postClone: string;
postUpdate: string;
prePublishSpec: string;
};
};
defaults?: {
branch?: string;
targetDir?: string;
openApiFile?: string;
stainlessConfigFile?: string;
guessConfig?: boolean;
projectName?: string;
};
}
Target Directory Templates
The targetDir
configuration supports template variables that are dynamically replaced when generating SDKs:
{sdk}
: Replaced with the name of the SDK being generated
{env}
: Replaced with the current environment (staging
or prod
)
{branch}
: Replaced with the git branch name (forward slashes are converted to hyphens for filesystem compatibility)
This allows you to organize your SDKs in a structured way. For example:
{
defaults: {
targetDir: './sdks/{env}/{sdk}/{branch}'
}
}
Would generate directories like:
./sdks/staging/typescript/main
./sdks/prod/typescript/main
./sdks/staging/python/theogravity-dev
(from branch theogravity/dev
)
This is particularly useful when working with multiple SDKs, environments, and branches simultaneously. Note that forward slashes in branch names are automatically converted to hyphens to ensure filesystem compatibility across different platforms.
Example Configuration
module.exports = {
stainlessSdkRepos: {
typescript: {
staging: 'git@github.com:stainless-sdks/yourproject-typescript-staging.git',
prod: 'git@github.com:stainless-sdks/yourproject-typescript.git',
},
},
defaults: {
branch: 'main',
targetDir: './sdks/{env}/{sdk}/{branch}',
openApiFile: './specs/openapi.yml',
stainlessConfigFile: './stainless-tools.config.yml',
projectName: 'my-project',
guessConfig: false
}
};
Lifecycle Hooks
The tool supports lifecycle hooks that allow you to automate tasks after certain operations. Currently supported hooks:
prePublishSpec
The prePublishSpec
hook runs before:
- Publishing OpenAPI and Stainless configuration files to Stainless
This hook is useful for:
- Validating OpenAPI specifications
- Running linters
- Transforming specifications
- Running pre-publish checks
postClone
The postClone
hook runs after:
- Initial clone of an SDK repository
This hook is useful for:
- Installing dependencies
- Running initial build scripts
- Setting up the development environment
postUpdate
The postUpdate
hook runs after:
- Pulling new changes from the remote repository
- And after restoring any stashed local changes (if there were any)
This hook is useful for:
- Rebuilding after updates
- Running database migrations
- Updating dependencies
- Running tests against new changes
Lifecycle Hook Configuration
Hooks are configured in the lifecycle
section of your configuration file:
module.exports = {
stainlessSdkRepos: {
typescript: {
staging: 'git@github.com:stainless-sdks/test-typescript.git',
prod: 'git@github.com:test-org/test-typescript.git',
},
},
lifecycle: {
typescript: {
prePublishSpec: 'npm run validate-spec',
postClone: 'cd $STAINLESS_TOOLS_SDK_PATH && npm install && npm run build',
postUpdate: 'npm run build',
},
python: {
prePublishSpec: 'python scripts/validate_spec.py',
postClone: 'python -m venv venv && source venv/bin/activate && pip install -e .',
postUpdate: 'source venv/bin/activate && pip install -e .',
},
},
defaults: {
branch: 'main',
targetDir: './sdks/{env}/{sdk}/{branch}',
openApiFile: './specs/openapi.yml',
stainlessConfigFile: './stainless-tools.config.yml',
guessConfig: false,
projectName: 'my-project'
}
};
The hook commands:
- Run from the SDK repository directory
- Have access to a shell (so you can use &&, ||, etc.)
- Will cause the tool to exit with an error if the command fails
- Have access to the following environment variables:
STAINLESS_TOOLS_SDK_PATH
: Full path to the cloned SDK repository
STAINLESS_TOOLS_SDK_BRANCH
: Name of the current branch
STAINLESS_TOOLS_SDK_REPO_NAME
: Name of the SDK repository (e.g., "typescript", "python")
Example using environment variables:
module.exports = {
lifecycle: {
typescript: {
postClone: 'cd $STAINLESS_TOOLS_SDK_PATH && npm install && npm run build',
postUpdate: 'echo "Updating SDK on branch $STAINLESS_TOOLS_SDK_BRANCH" && npm run build'
}
}
};
You can also use external scripts:
module.exports = {
lifecycle: {
typescript: {
postClone: './scripts/setup-sdk.sh',
postUpdate: './scripts/update-sdk.sh'
}
}
};
Example setup script (setup-sdk.sh
):
#!/bin/bash
echo "Setting up $STAINLESS_TOOLS_SDK_REPO_NAME SDK in $STAINLESS_TOOLS_SDK_PATH"
cd "$STAINLESS_TOOLS_SDK_PATH"
npm install
npm run build
Generate Command
The generate
command is the primary feature of Stainless Tools. It will clone (or update if exists) an SDK repository to a target directory, publish the OpenAPI file and Stainless Config file to the Stainless Config repo, and continuously monitor for changes to the SDK repository, pulling in new changes when detected.
Usage
stainless-tools generate [options] <sdk-name>
Arguments:
sdk-name Name of the SDK to generate
Options:
-b, --branch <name> Branch name to use (optional)
-t, --target-dir <dir> Target directory for the SDK (required if not in config)
-o, --open-api-file <file> OpenAPI specification file (required if not in config)
-c, --config <file> Configuration file path
-s, --stainless-config-file <file> Stainless configuration file (required if not in config)
-p, --project-name <name> Project name for Stainless API (required if not in config)
-g, --guess-config Uses the "Guess with AI" command from the Stainless Studio for the Stainless Config if enabled
--prod Use production URLs instead of staging URLs
-h, --help Display help for command
```bash
stainless-tools generate typescript
stainless-tools generate typescript \
--prod \
--branch main
stainless-tools generate \
--open-api-file ./api-spec.json \
--project-name my-project \
typescript
stainless-tools generate \
--prod \
--branch main \
--target-dir ./sdks/typescript \
--open-api-file ./api-spec.json \
--project-name my-project \
--config ./stainless-tools.config.js \
--guess-config \
typescript
Branch Configuration and Environments
Each SDK supports two environments with separate repositories:
staging
: For development and testing, typically using yourusername/dev
branch
prod
: For production releases, typically using main
branch
The branch name is completely optional and will be determined in this order:
- Command line:
--branch yourusername/dev
- Environment:
STAINLESS_SDK_BRANCH=yourusername/dev
- Config defaults:
defaults.branch
in your config file
- Current branch in target directory (if it exists)
- Generate a new random
cli/<random-hex>
branch
Example configuration:
{
"stainlessSdkRepos": {
"typescript": {
"staging": "git@github.com:stainless-sdks/my-api-typescript.git",
"prod": "git@github.com:my-org/my-api-typescript.git"
}
},
"defaults": {
"branch": "yourusername/dev" // Optional default branch
}
}
Basic usage:
stainless-tools generate typescript
stainless-tools generate typescript \
--prod \
--branch main
stainless-tools generate typescript
How It Works
When you run generate
:
-
Branch discovery:
- Checks for branch name in this order:
- Command-line flag (
--branch
)
- Environment variable (
STAINLESS_SDK_BRANCH
)
- Configuration default (
defaults.branch
)
- Current branch in target directory (if it exists)
- Generates a new random
cli/<random-hex>
branch
-
If you provide an OpenAPI file (--open-api-file
) or Stainless config file (--stainless-config-file
):
- Publishes these files to the Stainless Config repo via the Stainless API
- The API processes the files and generates the SDK in the specified branch
-
Repository initialization:
- If the directory doesn't exist:
- Creates and initializes a new git repository
- Waits for the branch to exist if it doesn't yet
- Once the branch exists, checks it out fresh
- If the directory exists:
- Verifies it's the correct repository
- If on a different branch:
- Stashes any local changes
- Waits for the target branch if it doesn't exist
- Switches to the branch once it exists
- Restores any stashed changes
- Pulls latest changes
-
Continuous monitoring:
- Watches for OpenAPI and config file changes:
- Automatically publishes changes to Stainless
- Waits for SDK regeneration
- Monitors SDK repository for updates:
- Pulls new changes when detected
- Handles local changes by:
- Stashing before updates
- Reapplying after updates
- Providing clear instructions if conflicts occur
- Executes lifecycle hooks:
- Runs
postClone
after initial setup
- Runs
postUpdate
after pulling changes
-
Handles interruptions gracefully:
- Ctrl+C stops the monitoring process
- Restores any stashed changes before exit
- Cleans up watchers and temporary state
This workflow ensures:
- Seamless coordination with CI/CD systems that create branches
- Safe handling of local changes during updates
- Proper sequencing of publish and branch operations
- Clear feedback during long-running operations
- Graceful handling of all error cases
Publish Specs Command
The publish-specs
command allows you to publish your OpenAPI specification and Stainless configuration directly to Stainless, making it easy to update your SDK configuration without using the web interface.
Usage
stainless-tools publish-specs [options] <sdk-name>
Arguments:
sdk-name Name of the SDK to publish specifications for
Options:
-b, --branch <branch> Git branch to use (optional)
-t, --target-dir <dir> Directory where the SDK will be generated
-o, --open-api-file <file> Path to OpenAPI specification file
-c, --config <file> Path to configuration file
-s, --stainless-config-file <file> Path to Stainless-specific configuration
-p, --project-name <name> Name of the project in Stainless
-g, --guess-config Use AI to guess configuration
--prod Use production URLs instead of staging
Examples
stainless-tools publish-specs typescript
stainless-tools publish-specs typescript \
--branch main \
--open-api-file openapi.json \
--config config.json
stainless-tools publish-specs \
--project-name my-project \
--open-api-file openapi.json \
typescript
stainless-tools publish-specs \
--stainless-config-file stainless.config.json \
--open-api-file openapi.json \
typescript
stainless-tools publish-specs typescript \
--prod \
--branch main \
--open-api-file openapi.json
When you run publish-specs
:
- The command validates your input and configuration
- Reads the OpenAPI specification file
- Reads the Stainless configuration file (if provided)
- Validates the configuration format
- Publishes the files to Stainless using your API key
The publish-specs command is particularly useful for:
- CI/CD pipelines where you want to automate SDK updates
- Local development when you want to quickly update your SDK configuration
- Testing new API changes before committing them to production