Skip to content
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 packages/cli/src/projects/checkout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +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;
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 = await ws.getCheckedOutProject();
Comment thread
doc-han marked this conversation as resolved.
Outdated
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
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/project/src/Workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export class Workspace {
return this.projectPaths.get(id);
}

getActiveProject() {
getTrackedProject() {
return (
this.projects.find((p) => p.openfn?.uuid === this.activeProject?.uuid) ??
this.projects.find((p) => p.id === this.activeProject?.id)
Expand Down
Loading