|
| 1 | +# Refactoring Commands into the Project Subcommand |
| 2 | + |
| 3 | +This guide outlines the steps to refactor an existing CLI command into a subcommand under the `project` command structure. |
| 4 | + |
| 5 | +## Example Reference |
| 6 | + |
| 7 | +Use `src/projects/list.ts` as the reference pattern for all refactored commands. |
| 8 | + |
| 9 | +## Steps |
| 10 | + |
| 11 | +### 1. Create the New Command File |
| 12 | + |
| 13 | +Create a new file in `src/projects/<command-name>.ts` that consolidates all the existing command files. |
| 14 | + |
| 15 | +**Key elements:** |
| 16 | +- Import `yargs`, `ensure`, `build`, `Logger`, and options from `../options` |
| 17 | +- Define a `<CommandName>Options` type that picks required fields from `Opts` |
| 18 | +- Create an `options` array with the required options (e.g., `[o.workflow, o.workspace, o.workflowMappings]`) |
| 19 | +- Export a default `command` object with: |
| 20 | + - `command: '<command-name> [args]'` (just the subcommand name, not the full path) |
| 21 | + - `describe: 'description of the command'` |
| 22 | + - `handler: ensure('project-<command-name>', options)` (note the `project-` prefix) |
| 23 | + - `builder: (yargs) => build(options, yargs)` |
| 24 | +- Export a named `handler` function that takes `(options: <CommandName>Options, logger: Logger)` |
| 25 | + |
| 26 | +**Example structure:** |
| 27 | +```typescript |
| 28 | +import yargs from 'yargs'; |
| 29 | +import { Workspace } from '@openfn/project'; |
| 30 | + |
| 31 | +import { ensure, build } from '../util/command-builders'; |
| 32 | +import type { Logger } from '../util/logger'; |
| 33 | +import * as o from '../options'; |
| 34 | +import type { Opts } from '../options'; |
| 35 | + |
| 36 | +export type VersionOptions = Required< |
| 37 | + Pick<Opts, 'command' | 'workflow' | 'workspace' | 'workflowMappings' | 'json'> |
| 38 | +>; |
| 39 | + |
| 40 | +const options = [o.workflow, o.workspace, o.workflowMappings]; |
| 41 | + |
| 42 | +const command: yargs.CommandModule = { |
| 43 | + command: 'version [workflow]', |
| 44 | + describe: 'Returns the version hash of a given workflow in a workspace', |
| 45 | + handler: ensure('project-version', options), |
| 46 | + builder: (yargs) => build(options, yargs), |
| 47 | +}; |
| 48 | + |
| 49 | +export default command; |
| 50 | + |
| 51 | +export const handler = async (options: VersionOptions, logger: Logger) => { |
| 52 | + // Implementation here |
| 53 | +}; |
| 54 | +``` |
| 55 | + |
| 56 | +### 2. Update `src/projects/handler.ts` |
| 57 | + |
| 58 | +Add a named export for your new handler: |
| 59 | + |
| 60 | +```typescript |
| 61 | +export { handler as list } from './list'; |
| 62 | +export { handler as version } from './version'; |
| 63 | +export { handler as <commandName> } from './<commandName>'; |
| 64 | +``` |
| 65 | + |
| 66 | +### 3. Update `src/projects/command.ts` |
| 67 | + |
| 68 | +Import and register your new command: |
| 69 | + |
| 70 | +```typescript |
| 71 | +import list from './list'; |
| 72 | +import version from './version'; |
| 73 | +import <commandName> from './<commandName>'; |
| 74 | + |
| 75 | +export const projectsCommand = { |
| 76 | + // ... |
| 77 | + builder: (yargs: yargs.Argv) => |
| 78 | + yargs |
| 79 | + .command(list) |
| 80 | + .command(version) |
| 81 | + .command(<commandName>) |
| 82 | + // ... |
| 83 | +}; |
| 84 | +``` |
| 85 | + |
| 86 | +### 4. Update `src/commands.ts` |
| 87 | + |
| 88 | +Make three changes: |
| 89 | + |
| 90 | +**a) Remove the old import** (if it exists): |
| 91 | +```typescript |
| 92 | +// Remove: import commandName from './<command>/handler'; |
| 93 | +``` |
| 94 | + |
| 95 | +**b) Add to CommandList type:** |
| 96 | +```typescript |
| 97 | +export type CommandList = |
| 98 | + | 'apollo' |
| 99 | + // ... |
| 100 | + | 'project-list' |
| 101 | + | 'project-version' |
| 102 | + | 'project-<command-name>' // Add this line |
| 103 | + | 'test' |
| 104 | + | 'version'; |
| 105 | +``` |
| 106 | + |
| 107 | +**c) Add to handlers object:** |
| 108 | +```typescript |
| 109 | +const handlers = { |
| 110 | + // ... |
| 111 | + ['project-list']: projects.list, |
| 112 | + ['project-version']: projects.version, |
| 113 | + ['project-<command-name>']: projects.<commandName>, // Add this line |
| 114 | + // ... |
| 115 | +}; |
| 116 | +``` |
| 117 | + |
| 118 | +**d) Update any existing handler references:** |
| 119 | +If the old command was referenced elsewhere in the handlers object (like `project: workflowVersion`), update or remove it as needed. |
| 120 | + |
| 121 | +### 5. Delete the Old Command Folder |
| 122 | + |
| 123 | +Remove the old command directory: |
| 124 | +```bash |
| 125 | +rm -rf packages/cli/src/<command-name> |
| 126 | +``` |
| 127 | + |
| 128 | +## Optional: Aliasing Commands at Top Level |
| 129 | + |
| 130 | +If you want to keep the command available at the top level (e.g., `openfn merge` in addition to `openfn project merge`), follow these additional steps. |
| 131 | + |
| 132 | +**Important:** When aliasing, the alias only exists in `src/cli.ts` where yargs commands are registered. Do NOT add separate handlers or command list entries in `src/commands.ts` - only keep the `project-<command-name>` entries there. |
| 133 | + |
| 134 | +### 6. Update `src/cli.ts` |
| 135 | + |
| 136 | +Import the command directly from the project subcommand file and register it: |
| 137 | + |
| 138 | +```typescript |
| 139 | +import projectsCommand from './projects/command'; |
| 140 | +import mergeCommand from './projects/merge'; |
| 141 | +import checkoutCommand from './checkout/command'; |
| 142 | +``` |
| 143 | + |
| 144 | +The command will be registered with yargs: |
| 145 | +```typescript |
| 146 | +.command(projectsCommand) |
| 147 | +.command(mergeCommand) // Top-level alias |
| 148 | +.command(checkoutCommand) |
| 149 | +``` |
| 150 | + |
| 151 | +### How It Works |
| 152 | + |
| 153 | +- In `src/cli.ts`: Both `merge` and `project merge` are registered as yargs commands |
| 154 | +- In `src/commands.ts`: Only `project-merge` exists in CommandList and handlers (no `merge` entry) |
| 155 | +- The yargs command handler (`ensure('project-merge', options)`) routes both command invocations to the same handler |
| 156 | + |
| 157 | +This is different from the `install`/`repo install` pattern, where both entries exist in the handlers object. |
| 158 | + |
| 159 | +## Common Patterns |
| 160 | + |
| 161 | +### Options to Use |
| 162 | +- Always include `o.workspace` for project-related commands |
| 163 | +- Use `o.workflow` for workflow-specific operations |
| 164 | +- Include `o.json` if the command supports JSON output |
| 165 | +- Include `o.log` for commands that need detailed logging |
| 166 | + |
| 167 | +### Handler Pattern |
| 168 | +- Use `new Workspace(options.workspace)` to access the workspace |
| 169 | +- Check `workspace.valid` before proceeding |
| 170 | +- Use `workspace.getActiveProject()` to get the current project |
| 171 | +- Use appropriate logger methods: `logger.info()`, `logger.error()`, `logger.success()` |
| 172 | + |
| 173 | +### Testing Pattern |
| 174 | +If the old command has tests, they need to be refactored: |
| 175 | + |
| 176 | +1. **Create new test file**: Move from `test/<command>/handler.test.ts` to `test/projects/<command>.test.ts` |
| 177 | +2. **Update imports**: |
| 178 | + - Change `import handler from '../../src/<command>/handler'` |
| 179 | + - To `import { handler } from '../../src/projects/<command>'` |
| 180 | +3. **Update command names in tests**: Change all `command: '<command>'` to `command: 'project-<command>'` in test option objects |
| 181 | +4. **Delete old test folder**: Remove `test/<command>` directory |
| 182 | + |
| 183 | +**Example changes:** |
| 184 | +```typescript |
| 185 | +// Before: |
| 186 | +import mergeHandler from '../../src/merge/handler'; |
| 187 | +await mergeHandler({ command: 'merge', ... }, logger); |
| 188 | + |
| 189 | +// After: |
| 190 | +import { handler as mergeHandler } from '../../src/projects/merge'; |
| 191 | +await mergeHandler({ command: 'project-merge', ... }, logger); |
| 192 | +``` |
| 193 | + |
| 194 | +## Checklist |
| 195 | + |
| 196 | +### Basic Refactoring |
| 197 | +- [ ] Create new file in `src/projects/<command-name>.ts` |
| 198 | +- [ ] Define `<CommandName>Options` type |
| 199 | +- [ ] Export default command object with `ensure('project-<command-name>', options)` |
| 200 | +- [ ] Export named `handler` function |
| 201 | +- [ ] Add export to `src/projects/handler.ts` |
| 202 | +- [ ] Import and register in `src/projects/command.ts` |
| 203 | +- [ ] Add `'project-<command-name>'` to `CommandList` in `src/commands.ts` |
| 204 | +- [ ] Add handler to handlers object in `src/commands.ts` |
| 205 | +- [ ] Remove old import from `src/commands.ts` if it exists |
| 206 | +- [ ] Delete old command folder |
| 207 | + |
| 208 | +### Testing (if applicable) |
| 209 | +- [ ] Create new test file in `test/projects/<command-name>.test.ts` |
| 210 | +- [ ] Update import to use `{ handler } from '../../src/projects/<command-name>'` |
| 211 | +- [ ] Update all `command: '<command>'` to `command: 'project-<command>'` in test cases |
| 212 | +- [ ] Delete old test folder `test/<command>` |
| 213 | +- [ ] Run tests to verify they pass |
| 214 | + |
| 215 | +### Additional Steps for Top-Level Aliasing |
| 216 | +- [ ] Import command directly in `src/cli.ts` (e.g., `import mergeCommand from './projects/merge'`) |
| 217 | +- [ ] Register the imported command with `.command(mergeCommand)` |
| 218 | +- [ ] Verify NO duplicate entries in `src/commands.ts` - only `project-<command-name>` should exist, not `<command-name>` |
| 219 | +- [ ] Test both `openfn <command>` and `openfn project <command>` |
0 commit comments