Skip to content

Commit 3e2a134

Browse files
* refactor list command * fix test * refactor version command * refactor merge command * refactor checkout command * update docs * types * tweaks * changeset * fix test * include workspace in merge command * fix other tests * update tests * feat: init fetch command * fooling around * refactor * little refactor of auth logic for a better life * add a fetch test * changeset * better project v2 serialization Sorting, strip nulls * add basic fetch test * refactoring * custom output * warn if projects have diverged on fetch Still some tests to update * fix tests * types * add force flag * simplify beta pull handler * fix test * types refactor * clean up pull types * typings * fix integration tests * if a project has no history, fetch without error * comment * changeset * clean up output path * workspace: should still work if no openfn.yaml is present * relax checkout to work with an invalid workspace * CLI: nicer handling of expected CLI errors * typings * version: [email protected] --------- Co-authored-by: Farhan Yahaya <[email protected]>
1 parent 2a56431 commit 3e2a134

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1453
-518
lines changed

.claude/command-refactor.md

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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>`

integration-tests/cli/test/project-v1.test.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ test.before(async () => {
110110
});
111111

112112
test.serial('list available projects', async (t) => {
113-
const { stdout } = await run(`openfn projects -p ${projectsPath}`);
113+
const { stdout } = await run(`openfn projects -w ${projectsPath}`);
114114

115115
t.regex(stdout, /hello-world/);
116116
t.regex(stdout, /8dbc4349-52b4-4bf2-be10-fdf06da52c46/);
@@ -120,7 +120,7 @@ test.serial('list available projects', async (t) => {
120120

121121
// checkout a project from a yaml file
122122
test.serial('Checkout a project', async (t) => {
123-
await run(`openfn checkout hello-world -p ${projectsPath}`);
123+
await run(`openfn checkout hello-world -w ${projectsPath}`);
124124

125125
// check workflow.yaml
126126
const workflowYaml = await readFile(
@@ -166,9 +166,7 @@ test.serial('merge a project', async (t) => {
166166
t.is(initial, '// TODO');
167167

168168
// Run the merge
169-
const { stdout } = await run(
170-
`openfn merge hello-world-staging -p ${projectsPath} --force`
171-
);
169+
await run(`openfn merge hello-world-staging -w ${projectsPath} --force`);
172170

173171
// Check the step is updated
174172
const merged = await readStep();

integration-tests/cli/test/project-v2.test.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ options:
2424
allow_support_access: false
2525
requires_mfa: false
2626
retention_policy: retain_all
27-
version_history: []
2827
workflows:
2928
- name: Hello Workflow
3029
steps:
@@ -80,8 +79,6 @@ options:
8079
allow_support_access: false
8180
requires_mfa: false
8281
retention_policy: retain_all
83-
version_history:
84-
- 7b0f5af558f5
8582
workflows:
8683
- name: Hello Workflow
8784
steps:
@@ -130,15 +127,15 @@ test.before(async () => {
130127
});
131128

132129
test.serial('list available projects', async (t) => {
133-
const { stdout } = await run(`openfn projects -p ${projectsPath}`);
130+
const { stdout } = await run(`openfn projects -w ${projectsPath}`);
134131
t.regex(stdout, /sandboxing-simple/);
135132
t.regex(stdout, /a272a529-716a-4de7-a01c-a082916c6d23/);
136133
t.regex(stdout, /staging/);
137134
t.regex(stdout, /bc6629fb-7dc8-4b28-93af-901e2bd58dc4/);
138135
});
139136

140137
test.serial('Checkout a project', async (t) => {
141-
await run(`openfn checkout staging -p ${projectsPath}`);
138+
await run(`openfn checkout staging -w ${projectsPath}`);
142139

143140
// check workflow.yaml
144141
const workflowYaml = await readFile(
@@ -149,8 +146,7 @@ test.serial('Checkout a project', async (t) => {
149146
workflowYaml,
150147
`id: hello-workflow
151148
name: Hello Workflow
152-
options:
153-
history: []
149+
options: {}
154150
steps:
155151
- id: trigger
156152
type: webhook
@@ -180,15 +176,15 @@ test.serial('merge a project', async (t) => {
180176
'utf8'
181177
).then((str) => str.trim());
182178

183-
await run(`openfn checkout sandboxing-simple -p ${projectsPath}`);
179+
await run(`openfn checkout sandboxing-simple -w ${projectsPath}`);
184180

185181
// assert the initial step code
186182
const initial = await readStep();
187183
t.is(initial, '// TODO');
188184

189185
// Run the merge
190186
const { stdout } = await run(
191-
`openfn merge staging -p ${projectsPath} --force`
187+
`openfn merge staging -w ${projectsPath} --force`
192188
);
193189

194190
// Check the step is updated

packages/cli/CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
# @openfn/cli
22

3+
## 1.20.0
4+
5+
### Minor Changes
6+
7+
- 69ec22a: Refactor of openfn project command. There are very few user-facing changes, and they should be compatible
8+
9+
- A new `project` namespace has been set up, allowing `openfn project version|list|merge|checkout`
10+
- `openfn projects` will continue to list projects in the workspace (but is just an alias of list)
11+
- The prior `openfn merge|checkout` command still exist, it just aliases to `openfn projct merge|checkout`
12+
13+
One change to watch out for is that `--project-path` has been changed to `--workspace`, which can also be set through `-w` and `OPENFN_WORKSPACE`.
14+
15+
- 162e0ea: Add a fetch command, which will download a project from an app but not check it out. This will throw if the local project version has diverged from the remote version.
16+
17+
Rebased `pull --beta` to simply be fetch & checkout
18+
319
## 1.19.0
420

521
### Minor Changes

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openfn/cli",
3-
"version": "1.19.0",
3+
"version": "1.20.0",
44
"description": "CLI devtools for the OpenFn toolchain",
55
"engines": {
66
"node": ">=18",

packages/cli/src/checkout/command.ts

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)