Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 16 additions & 2 deletions .claude/command-refactor.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Use `src/projects/list.ts` as the reference pattern for all refactored commands.
Create a new file in `src/projects/<command-name>.ts` that consolidates all the existing command files.

**Key elements:**

- Import `yargs`, `ensure`, `build`, `Logger`, and options from `../options`
- Define a `<CommandName>Options` type that picks required fields from `Opts`
- Create an `options` array with the required options (e.g., `[o.workflow, o.workspace, o.workflowMappings]`)
Expand All @@ -24,6 +25,7 @@ Create a new file in `src/projects/<command-name>.ts` that consolidates all the
- Export a named `handler` function that takes `(options: <CommandName>Options, logger: Logger)`

**Example structure:**

```typescript
import yargs from 'yargs';
import { Workspace } from '@openfn/project';
Expand Down Expand Up @@ -88,23 +90,26 @@ export const projectsCommand = {
Make three changes:

**a) Remove the old import** (if it exists):

```typescript
// Remove: import commandName from './<command>/handler';
```

**b) Add to CommandList type:**

```typescript
export type CommandList =
| 'apollo'
// ...
| 'project-list'
| 'project-version'
| 'project-<command-name>' // Add this line
| 'project-<command-name>' // Add this line
| 'test'
| 'version';
```

**c) Add to handlers object:**

```typescript
const handlers = {
// ...
Expand All @@ -121,6 +126,7 @@ If the old command was referenced elsewhere in the handlers object (like `projec
### 5. Delete the Old Command Folder

Remove the old command directory:

```bash
rm -rf packages/cli/src/<command-name>
```
Expand All @@ -142,6 +148,7 @@ import checkoutCommand from './checkout/command';
```

The command will be registered with yargs:

```typescript
.command(projectsCommand)
.command(mergeCommand) // Top-level alias
Expand All @@ -159,18 +166,21 @@ This is different from the `install`/`repo install` pattern, where both entries
## Common Patterns

### Options to Use

- Always include `o.workspace` for project-related commands
- Use `o.workflow` for workflow-specific operations
- Include `o.json` if the command supports JSON output
- Include `o.log` for commands that need detailed logging

### Handler Pattern

- Use `new Workspace(options.workspace)` to access the workspace
- Check `workspace.valid` before proceeding
- Use `workspace.getActiveProject()` to get the current project
- Use `workspace.getTrackedProject()` to get the current project
- Use appropriate logger methods: `logger.info()`, `logger.error()`, `logger.success()`

### Testing Pattern

If the old command has tests, they need to be refactored:

1. **Create new test file**: Move from `test/<command>/handler.test.ts` to `test/projects/<command>.test.ts`
Expand All @@ -181,6 +191,7 @@ If the old command has tests, they need to be refactored:
4. **Delete old test folder**: Remove `test/<command>` directory

**Example changes:**

```typescript
// Before:
import mergeHandler from '../../src/merge/handler';
Expand All @@ -194,6 +205,7 @@ await mergeHandler({ command: 'project-merge', ... }, logger);
## Checklist

### Basic Refactoring

- [ ] Create new file in `src/projects/<command-name>.ts`
- [ ] Define `<CommandName>Options` type
- [ ] Export default command object with `ensure('project-<command-name>', options)`
Expand All @@ -206,13 +218,15 @@ await mergeHandler({ command: 'project-merge', ... }, logger);
- [ ] Delete old command folder

### Testing (if applicable)

- [ ] Create new test file in `test/projects/<command-name>.test.ts`
- [ ] Update import to use `{ handler } from '../../src/projects/<command-name>'`
- [ ] Update all `command: '<command>'` to `command: 'project-<command>'` in test cases
- [ ] Delete old test folder `test/<command>`
- [ ] Run tests to verify they pass

### Additional Steps for Top-Level Aliasing

- [ ] Import command directly in `src/cli.ts` (e.g., `import mergeCommand from './projects/merge'`)
- [ ] Register the imported command with `.command(mergeCommand)`
- [ ] Verify NO duplicate entries in `src/commands.ts` - only `project-<command-name>` should exist, not `<command-name>`
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:22-alpine AS base
FROM node:24-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
Expand Down
19 changes: 10 additions & 9 deletions integration-tests/worker/test/exit-reasons.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const run = async (attempt) => {
});
};

test('crash: syntax error', async (t) => {
test.serial('crash: syntax error', async (t) => {
const attempt = {
id: crypto.randomUUID(),
jobs: [
Expand All @@ -54,7 +54,7 @@ test('crash: syntax error', async (t) => {
});

// https://github.com/OpenFn/kit/issues/1045
test('crash: reference error', async (t) => {
test.serial('crash: reference error', async (t) => {
const attempt = {
id: crypto.randomUUID(),
jobs: [
Expand All @@ -78,7 +78,7 @@ test('crash: reference error', async (t) => {
});

// https://github.com/OpenFn/kit/issues/758
test('crash: job not found', async (t) => {
test.serial('crash: job not found', async (t) => {
lightning.addDataclip('x', {});

const attempt = {
Expand All @@ -102,7 +102,7 @@ test('crash: job not found', async (t) => {
t.regex(error_message, /could not find start job: y/i);
});

test('exception: autoinstall error', async (t) => {
test.serial('exception: autoinstall error', async (t) => {
const attempt = {
id: crypto.randomUUID(),
jobs: [
Expand All @@ -125,7 +125,7 @@ test('exception: autoinstall error', async (t) => {
);
});

test('exception: bad credential (not found)', async (t) => {
test.serial('exception: bad credential (not found)', async (t) => {
const attempt = {
id: crypto.randomUUID(),
jobs: [
Expand Down Expand Up @@ -154,7 +154,7 @@ test('exception: bad credential (not found)', async (t) => {
);
});

test('exception: credential timeout', async (t) => {
test.serial('exception: credential timeout', async (t) => {
const attempt = {
id: crypto.randomUUID(),
jobs: [
Expand All @@ -177,7 +177,7 @@ test('exception: credential timeout', async (t) => {
);
});

test('kill: oom (small, kill worker)', async (t) => {
test.serial('kill: oom (small, kill worker)', async (t) => {
const attempt = {
id: crypto.randomUUID(),
jobs: [
Expand All @@ -201,7 +201,8 @@ test('kill: oom (small, kill worker)', async (t) => {
t.is(error_message, 'Run exceeded maximum memory usage');
});

test('kill: oom (large, kill vm)', async (t) => {
// TODO this is failing locally... is it OK in CI?
test.serial('kill: oom (large, kill vm)', async (t) => {
const attempt = {
id: crypto.randomUUID(),
jobs: [
Expand All @@ -225,7 +226,7 @@ test('kill: oom (large, kill vm)', async (t) => {
t.is(error_message, 'Run exceeded maximum memory usage');
});

test('crash: process.exit() triggered by postgres', async (t) => {
test.serial('crash: process.exit() triggered by postgres', async (t) => {
const attempt = {
id: crypto.randomUUID(),
jobs: [
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @openfn/cli

## 1.33.0

### Minor Changes

- 6829bdc: Remove renamed/deleted workflows or steps during checkout

## 1.32.0

### Minor Changes
Expand Down
2 changes: 1 addition & 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.32.0",
"version": "1.33.0",
"description": "CLI devtools for the OpenFn toolchain",
"engines": {
"node": ">=18",
Expand Down
5 changes: 2 additions & 3 deletions packages/cli/src/projects/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ 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();

const currentProject = await workspace.getCheckedOutProject();
// get the project
let switchProject;
if (/\.(yaml|json)$/.test(projectIdentifier)) {
Expand Down Expand Up @@ -108,7 +107,7 @@ export const handler = async (options: CheckoutOptions, logger?: Logger) => {
if (options.clean) {
await rimraf(workspace.workflowsPath);
} else {
await tidyWorkflowDir(currentProject!, switchProject);
await tidyWorkflowDir(currentProject, switchProject, false, workspacePath);
}

// write the forked from map
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/projects/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export async function handler(options: DeployOptions, logger: Logger) {
// We need track alias in openfn.yaml to make this easier (and tracked in from fs)
const ws = new Workspace(options.workspace || '.');

const active = ws.getActiveProject();
const active = ws.getTrackedProject();
const alias = options.alias ?? active?.alias;

const localProject = await Project.from('fs', {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/projects/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const handler = async (options: ProjectListOptions, logger: Logger) => {

logger.always(`Available openfn projects\n\n${workspace
.list()
.map((p) => describeProject(p, p === workspace.getActiveProject()))
.map((p) => describeProject(p, p === workspace.getTrackedProject()))
.join('\n\n')}
`);
};
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/projects/merge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const handler = async (options: MergeOptions, logger: Logger) => {
logger.debug('Loading target project from path', basePath);
targetProject = await Project.from('path', basePath);
} else {
targetProject = workspace.getActiveProject()!;
targetProject = workspace.getTrackedProject()!;
if (!targetProject) {
logger.error(`No project currently checked out`);
return;
Expand Down
14 changes: 12 additions & 2 deletions packages/cli/src/projects/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'node:path';
import fs from 'node:fs';
import { mkdir, writeFile } from 'node:fs/promises';
import { Provisioner } from '@openfn/lexicon/lightning';
import { fetch } from 'undici';
Expand Down Expand Up @@ -214,7 +215,8 @@ class DeployError extends Error {
export async function tidyWorkflowDir(
currentProject: Project | undefined,
incomingProject: Project | undefined,
dryRun = false
dryRun = false,
dirPath = '.'
) {
if (!currentProject || !incomingProject) {
return [];
Expand All @@ -232,7 +234,15 @@ export async function tidyWorkflowDir(
}

if (!dryRun) {
await rimraf(toRemove);
const abs = (p: string) => path.join(dirPath, p);
await rimraf(toRemove.map(abs));

const dirs = new Set(toRemove.map((f) => path.dirname(f)));
for (const dir of Array.from(dirs)) {
try {
fs.rmdirSync(abs(dir)); // throws when not empty
} catch {}
}
}

// Return and sort for testing
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/projects/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const handler = async (options: VersionOptions, logger: Logger) => {

const output = new Map<string, string>();

const activeProject = workspace.getActiveProject();
const activeProject = await workspace.getCheckedOutProject();
if (options.workflow) {
const workflow = activeProject?.getWorkflow(options.workflow);
if (!workflow) {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/util/load-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const loadPlan = async (
};

options.credentials ??= workspace.getConfig().credentials;
options.collectionsEndpoint ??= proj.openfn?.endpoint;
options.collectionsEndpoint ??= proj?.openfn?.endpoint;
// Set the cache path to be relative to the workflow
options.cachePath ??= workspace.workflowsPath + `/${name}/${CACHE_DIR}`;
}
Expand Down
Loading