Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8b9f402
fetch raw v1 state files
josephjclark Jan 16, 2026
3af0ef7
make new command hidden
josephjclark Jan 16, 2026
0cc36cc
claude stuff
josephjclark Jan 19, 2026
325beee
start adding a test
josephjclark Jan 19, 2026
0c46311
add deploy test
josephjclark Jan 19, 2026
900ecf9
mock: return project
josephjclark Jan 19, 2026
7efe6aa
skip version checking if there's no version history
josephjclark Jan 19, 2026
a23e4e7
project: ensure history is tracked across workflow merge
josephjclark Jan 20, 2026
3e7b59d
disable divergence checking on deploy, and update tests
josephjclark Jan 20, 2026
5a575eb
changeset
josephjclark Jan 20, 2026
7b50224
on fetch and pull, use checked out project id
josephjclark Jan 20, 2026
26381fa
fix handling of start option in workflow.yaml
josephjclark Jan 20, 2026
5969c2d
only remove the required files on checkout
josephjclark Jan 20, 2026
022d314
handle expressions
josephjclark Jan 20, 2026
8c55995
changeset
josephjclark Jan 20, 2026
2505745
typo
josephjclark Jan 20, 2026
f298302
types
josephjclark Jan 20, 2026
0b1fcd3
project: add getters for workspace and project paths
josephjclark Jan 20, 2026
52a4fef
cli: set cache path if workflow name is used
josephjclark Jan 20, 2026
090fbf3
changesets
josephjclark Jan 20, 2026
1a61b8c
fix test
josephjclark Jan 20, 2026
17f73a0
types
josephjclark Jan 20, 2026
ddc304e
fix an issue resolving adaptor shorthands
josephjclark Jan 20, 2026
53cd4ac
fixed issue in cache path
josephjclark Jan 20, 2026
94f2809
improve gitignore tracking
josephjclark Jan 21, 2026
f50b744
fixes
josephjclark Jan 21, 2026
1e7e02f
break infinite loop case
josephjclark Jan 21, 2026
d59ca3b
tidy
josephjclark Jan 21, 2026
f2856c5
runtime: fix an issue on start
josephjclark Jan 21, 2026
058e341
logging
josephjclark Jan 21, 2026
57701db
version: cli@1.25.0 worker@1.21.3
josephjclark Jan 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions claude.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# OpenFn Kit

This monorepo contains the core packages that power OpenFn's workflow automation platform. OpenFn is a Digital Public Good trusted by NGOs and governments in 40+ countries to automate data integration workflows.

## Architecture

The repository has three main packages: **CLI**, **Runtime**, and **Worker**. The CLI and Worker are both frontends for executing workflows - the CLI for local development, the Worker for production execution via Lightning (the web platform). Both wrap the Runtime as their execution engine. The Worker uses engine-multi to wrap the Runtime for multi-process execution.

## Core Packages

- **[@openfn/cli](packages/cli)** - Command-line interface for local development. Run, test, compile, and deploy workflows.
- **[@openfn/runtime](packages/runtime)** - Core execution engine. Safely executes jobs in a sandboxed VM environment.
- **[@openfn/ws-worker](packages/ws-worker)** - WebSocket worker connecting Lightning to the Runtime. Stateless server that pulls runs from Lightning's queue. See [.claude/event-processor.md](.claude/event-processor.md) for event processing details.
- **[@openfn/engine-multi](packages/engine-multi)** - Multi-process runtime wrapper used by ws-worker for concurrent workflow execution.
- **[@openfn/compiler](packages/compiler)** - Transforms OpenFn job DSL into executable JavaScript modules.

## Supporting Packages

- **@openfn/lexicon** - Shared TypeScript types
- **@openfn/logger** - Structured logging utilities
- **@openfn/describe-package** - TypeScript analysis for adaptor docs (to be phased out)
- **@openfn/deploy** - Deployment logic for Lightning (soon to be deprecated)
- **@openfn/project** - Models and understands local OpenFn projects
- **@openfn/lightning-mock** - Mock Lightning server for testing

## AI Assistant

- Keep responses terse and do not over-explain. Users will ask for more guidance if they need it.
- Always present users a short action plan and ask for confirmation before doing it
- Keep the human in the loop at all times. Stop regularly and check for guidance.

## Key Concepts

**Workflows** are sequences of **jobs** that process data through steps. Each **job** is an array of **operations** (functions that transform state). State flows between jobs based on conditional edges.

**Adaptors** are npm packages (e.g., `@openfn/language-http`) providing operations for specific systems. The CLI auto-installs them as needed.

The **Compiler** transforms job DSL code into standard ES modules with imports and operation arrays.

## Development Setup

### Prerequisites

- Node.js 18+ (use `asdf`)
- pnpm (enable with `corepack enable`)

### Common Commands

```bash
# Root
pnpm install # Install dependencies
pnpm build # Build all packages
pnpm test # Run all tests
pnpm changeset # Add a changeset for your PR

# CLI
cd packages/cli
pnpm openfn test # Run from source
pnpm install:global # Install as 'openfnx' for testing

# Worker
cd packages/ws-worker
pnpm start # Connect to localhost:4000
pnpm start -l mock # Use mock Lightning
pnpm start --no-loop # Disable auto-fetch
curl -X POST http://localhost:2222/claim # Manual claim
```

### Environment Variables

- `OPENFN_REPO_DIR` - CLI adaptor storage
- `OPENFN_ADAPTORS_REPO` - Local adaptors monorepo path
- `OPENFN_API_KEY` - API key for Lightning deployment
- `OPENFN_ENDPOINT` - Lightning URL (default: app.openfn.org)
- `WORKER_SECRET` - Worker authentication secret

## Repository Structure

```
packages/
├── cli/ # CLI entry: cli.ts, commands.ts, projects/, options.ts
├── runtime/ # Runtime entry: index.ts, runtime.ts, util/linker
├── ws-worker/ # Worker entry: start.ts, server.ts, api/, events/
├── compiler/ # Job DSL compiler
├── engine-multi/ # Multi-process wrapper
├── lexicon/ # Shared TypeScript types
└── logger/ # Logging utilities
```

## Testing & Releases

```bash
pnpm test # All tests
pnpm test:types # Type checking
pnpm test:integration # Integration tests
cd packages/cli && pnpm test:watch # Watch mode
```

## Testing Best Practice

- Ensure tests are valuable before generating them. Focus on what's important.
- Treat tests as documentation: they should show how the function is expected to work
- Keep tests focuses: test one thing in each test
- This repo contains extensive testing: check for similar patterns in the same package before improvising

## Additional Documentation

**Changesets**: Run `pnpm changeset` when submitting PRs. Releases publish automatically to npm on merge to main.

The [.claude](.claude) folder contains detailed guides:

- **[command-refactor.md](.claude/command-refactor.md)** - Refactoring CLI commands into project subcommand structure
- **[event-processor.md](.claude/event-processor.md)** - Worker event processing architecture (batching, ordering)

## Code Standards

- **Formatting**: Use Prettier (`pnpm format`)
- **TypeScript**: Required for all new code
- **TypeSync**: Run `pnpm typesync` after modifying dependencies
- **Tests**: Write tests and run `pnpm build` before testing (tests run against `dist/`)
- **Independence**: Keep packages loosely coupled where possible

## Architecture Principles

- **Separation of Concerns**: CLI and Worker are frontends; Runtime is the shared execution backend
- **Sandboxing**: Runtime uses Node's VM module for isolation
- **State Immutability**: State cannot be mutated between jobs
- **Portability**: Compiled jobs are standard ES modules
- **Zero Persistence (Worker)**: Worker is stateless; Lightning handles persistence
- **Multi-Process Isolation**: Worker uses engine-multi for concurrent workflow execution

## Contributing

1. Make changes
2. Run `pnpm test`
3. Add changeset: `pnpm changeset`
4. Open PR at https://github.com/openfn/kit

**Resources**: [docs.openfn.org](https://docs.openfn.org) | [app.openfn.org](https://app.openfn.org) | [github.com/openfn/kit](https://github.com/openfn/kit)
10 changes: 10 additions & 0 deletions integration-tests/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# @openfn/integration-tests-cli

## 1.0.11

### Patch Changes

- Updated dependencies [090fbf3]
- Updated dependencies [a23e4e7]
- Updated dependencies [8c55995]
- @openfn/project@0.12.1
- @openfn/lightning-mock@2.4.4

## 1.0.10

### Patch Changes
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@openfn/integration-tests-cli",
"private": true,
"version": "1.0.10",
"version": "1.0.11",
"description": "CLI integration tests",
"author": "Open Function Group <admin@openfn.org>",
"license": "ISC",
Expand Down
19 changes: 19 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# @openfn/cli

## 1.25.0

### Minor Changes

- 8b9f402: fetch: allow state files to be writtem to JSON with --format

### Patch Changes

- 26381fa: Fix an issue where start is not correctly loaded from workflow.yaml
- 5a575eb: On deploy, skip the check to see if the remote history has diverged. History tracking still needs some work and this feature isn't working properly yet"
- 8c55995: When checking out new projects, only delete the files necessary
- 090fbf3: Fix step caching when running a workflow through the Project
- Updated dependencies [090fbf3]
- Updated dependencies [f2856c5]
- Updated dependencies [a23e4e7]
- Updated dependencies [8c55995]
- @openfn/project@0.12.1
- @openfn/runtime@1.8.3

## 1.24.1

### Patch Changes
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openfn/cli",
"version": "1.24.1",
"version": "1.25.0",
"description": "CLI devtools for the OpenFn toolchain",
"engines": {
"node": ">=18",
Expand Down Expand Up @@ -42,6 +42,7 @@
"@types/ws": "^8.18.1",
"@types/yargs": "^17.0.33",
"ava": "5.3.1",
"lodash-es": "^4.17.21",
"mock-fs": "^5.5.0",
"tslib": "^2.8.1",
"tsup": "^7.2.0",
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/execute/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import loadState from '../util/load-state';
import validateAdaptors from '../util/validate-adaptors';
import loadPlan from '../util/load-plan';
import assertPath from '../util/assert-path';
import { clearCache } from '../util/cache';
import { clearCache, getCachePath } from '../util/cache';
import fuzzyMatchStep from '../util/fuzzy-match-step';
import abort from '../util/abort';
import validatePlan from '../util/validate-plan';
Expand Down Expand Up @@ -182,7 +182,10 @@ const executeHandler = async (options: ExecuteOptions, logger: Logger) => {

if (options.cacheSteps) {
logger.success(
'Cached output written to ./cli-cache (see info logs for details)'
`Cached output written to ${getCachePath(
options,
plan.workflow.name
)} (see info logs for details)`
);
}

Expand Down
12 changes: 10 additions & 2 deletions packages/cli/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type Opts = {
json?: boolean;
beta?: boolean;
cacheSteps?: boolean;
cachePath?: string;
compile?: boolean;
configPath?: string;
confirm?: boolean;
Expand Down Expand Up @@ -218,6 +219,13 @@ export const cacheSteps: CLIOption = {
},
};

export const cacheDir: CLIOption = {
name: 'cache-dir',
yargs: {
description: 'Set the path to read/write the state cache',
},
};

export const compile: CLIOption = {
name: 'no-compile',
yargs: {
Expand Down Expand Up @@ -366,8 +374,8 @@ export const ignoreImports: CLIOption = {
},
};

const getBaseDir = (opts: { path?: string }) => {
const basePath = opts.path ?? '.';
const getBaseDir = (opts: { path?: string; workspace?: string }) => {
const basePath = opts.path ?? opts.workspace ?? '.';
if (/\.(jso?n?|ya?ml)$/.test(basePath)) {
return nodePath.dirname(basePath);
}
Expand Down
15 changes: 11 additions & 4 deletions packages/cli/src/projects/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,22 @@ import * as o from '../options';
import * as po from './options';

import type { Opts } from './options';
import { tidyWorkflowDir } from './util';

export type CheckoutOptions = Pick<
Opts,
'command' | 'project' | 'workspace' | 'log'
'command' | 'project' | 'workspace' | 'log' | 'clean'
>;

const options = [o.log, po.workspace];
const options = [o.log, po.workspace, po.clean];

const command: yargs.CommandModule = {
command: 'checkout <project>',
describe: 'Switch to a different OpenFn project in the same workspace',
handler: ensure('project-checkout', options),
builder: (yargs) =>
build(options, yargs).positional('project', {
describe: 'The id, alias or UUID of the project to chcekout',
describe: 'The id, alias or UUID of the project to checkout',
demandOption: true,
}),
};
Expand All @@ -40,6 +41,8 @@ export const handler = async (options: CheckoutOptions, logger: Logger) => {
// TODO: try to retain the endpoint for the projects
const { project: _, ...config } = workspace.getConfig() as any;

const currentProject = workspace.getActiveProject();

// get the project
let switchProject;
if (/\.(yaml|json)$/.test(projectIdentifier)) {
Expand All @@ -60,7 +63,11 @@ export const handler = async (options: CheckoutOptions, logger: Logger) => {
}

// delete workflow dir before expanding project
await rimraf(path.join(workspacePath, config.workflowRoot ?? 'workflows'));
if (options.clean) {
await rimraf(workspace.workflowsPath);
} else {
await tidyWorkflowDir(currentProject!, switchProject);
}

// expand project into directory
const files: any = switchProject.serialize('fs');
Expand Down
21 changes: 20 additions & 1 deletion packages/cli/src/projects/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ export async function handler(options: DeployOptions, logger: Logger) {
// Note that it's a little wierd to deploy a project you haven't checked out,
// so put good safeguards here
logger.info('Attempting to load checked-out project from workspace');

// TODO this doesn't have a history!
// loading from the fs the history isn't available
const localProject = await Project.from('fs', {
root: options.workspace || '.',
});
Expand Down Expand Up @@ -126,7 +129,19 @@ Pass --force to override this error and deploy anyway.`);
}

// Ensure there's no divergence
if (!localProject.canMergeInto(remoteProject!)) {

// Skip divergence testing if the remote has no history in its workflows
// (this will only happen on older versions of lightning)
const skipVersionTest =
localProject.workflows.find((wf) => wf.history.length === 0) ||
remoteProject.workflows.find((wf) => wf.history.length === 0);

if (skipVersionTest) {
logger.warn(
'Skipping compatibility check as no local version history detected'
);
logger.warn('Pushing these changes may overrite changes made to the app');
} else if (!localProject.canMergeInto(remoteProject!)) {
if (!options.force) {
logger.error(`Error: Projects have diverged!

Expand Down Expand Up @@ -168,6 +183,10 @@ Pass --force to override this error and deploy anyway.`);
if (options.dryRun) {
logger.always('dryRun option set: skipping upload step');
} else {
// sync summary
// :+1: the remove project has not changed since last sync / the remote project has changed since last sync, and your changes may overwrite these
// The following workflows will be updated

if (options.confirm) {
if (
!(await logger.confirm(
Expand Down
Loading