Skip to content

Commit c2d502f

Browse files
Enhance CLI commands with partial matching and formatting
Add partial-match utility for fuzzy loop name lookup. Introduce ralph-format helpers for consistent output formatting. Improve status, cancel, and list commands with better UX.
1 parent fbf259f commit c2d502f

18 files changed

Lines changed: 2446 additions & 661 deletions

packages/memory/src/cli/commands/cancel.ts

Lines changed: 104 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -3,79 +3,21 @@ import { openDatabase, confirm } from '../utils'
33
import { execSync, spawnSync } from 'child_process'
44
import { existsSync } from 'fs'
55
import { resolve } from 'path'
6+
import { findPartialMatch } from '../../utils/partial-match'
67

7-
interface CancelOptions {
8-
projectId?: string
8+
export interface CancelArgs {
99
dbPath?: string
10+
resolvedProjectId?: string
11+
name?: string
1012
cleanup?: boolean
1113
force?: boolean
12-
help?: boolean
1314
}
1415

15-
function parseArgs(args: string[]): CancelOptions & { worktreeName?: string } {
16-
const options: CancelOptions & { worktreeName?: string } = {}
17-
let i = 0
18-
19-
while (i < args.length) {
20-
const arg = args[i]
21-
22-
if (arg === '--project' || arg === '-p') {
23-
options.projectId = args[++i]
24-
} else if (arg === '--db-path') {
25-
options.dbPath = args[++i]
26-
} else if (arg === '--cleanup') {
27-
options.cleanup = true
28-
} else if (arg === '--force') {
29-
options.force = true
30-
} else if (arg === '--help' || arg === '-h') {
31-
options.help = true
32-
} else if (!arg.startsWith('-')) {
33-
options.worktreeName = arg
34-
} else {
35-
console.error(`Unknown option: ${arg}`)
36-
help()
37-
process.exit(1)
38-
}
39-
40-
i++
41-
}
42-
43-
return options
44-
}
45-
46-
export function help(): void {
47-
console.log(`
48-
Cancel a Ralph loop
49-
50-
Usage:
51-
ocm-mem cancel [name] [options]
52-
53-
Arguments:
54-
name Worktree name to cancel (optional if only one active)
55-
56-
Options:
57-
--cleanup Remove worktree directory after cancellation
58-
--force Skip confirmation prompt
59-
--project, -p <id> Project ID (auto-detected from git if not provided)
60-
--db-path <path> Path to memory database
61-
--help, -h Show this help message
62-
`.trim())
63-
}
64-
65-
export async function run(args: string[], globalOpts: { dbPath?: string; projectId?: string }): Promise<void> {
66-
const options = parseArgs(args)
67-
options.projectId = options.projectId || globalOpts.projectId
68-
options.dbPath = options.dbPath || globalOpts.dbPath
69-
70-
if (options.help) {
71-
help()
72-
process.exit(0)
73-
}
74-
75-
const db = openDatabase(options.dbPath)
16+
export async function run(argv: CancelArgs): Promise<void> {
17+
const db = openDatabase(argv.dbPath)
7618

7719
try {
78-
const projectId = options.projectId
20+
const projectId = argv.resolvedProjectId
7921

8022
const now = Date.now()
8123
let query: string
@@ -123,11 +65,23 @@ export async function run(args: string[], globalOpts: { dbPath?: string; project
12365

12466
let loopToCancel: { state: RalphState; row: { project_id: string; key: string; data: string } } | undefined
12567

126-
if (options.worktreeName) {
127-
loopToCancel = loops.find((l) => l.state.worktreeName === options.worktreeName)
68+
if (argv.name) {
69+
const { match, candidates } = findPartialMatch(argv.name, loops, (l) => [
70+
l.state.worktreeName,
71+
l.state.worktreeBranch,
72+
])
73+
74+
if (!match && candidates.length > 0) {
75+
console.error(`Multiple loops match '${argv.name}':`)
76+
for (const c of candidates) {
77+
console.error(` - ${c.state.worktreeName}`)
78+
}
79+
console.error('')
80+
process.exit(1)
81+
}
12882

129-
if (!loopToCancel) {
130-
console.error(`Ralph loop not found: ${options.worktreeName}`)
83+
if (!match && candidates.length === 0) {
84+
console.error(`Ralph loop not found: ${argv.name}`)
13185
console.error('')
13286
console.error('Active loops:')
13387
for (const l of loops) {
@@ -136,6 +90,8 @@ export async function run(args: string[], globalOpts: { dbPath?: string; project
13690
console.error('')
13791
process.exit(1)
13892
}
93+
94+
loopToCancel = match!
13995
} else {
14096
if (loops.length === 1) {
14197
loopToCancel = loops[0]
@@ -166,62 +122,100 @@ export async function run(args: string[], globalOpts: { dbPath?: string; project
166122
console.log(` Session: ${state.sessionId}`)
167123
console.log(` Iteration: ${state.iteration}/${state.maxIterations}`)
168124
console.log(` Phase: ${state.phase}`)
169-
if (options.cleanup) {
125+
if (argv.cleanup) {
170126
console.log(` Worktree: ${state.worktreeDir} (will be removed)`)
171127
}
172128
console.log('')
173129

174-
await runCancel(db, loopToCancel, options)
130+
const shouldProceed = argv.force || await confirm(`Cancel Ralph loop '${state.worktreeName}'`)
131+
132+
if (!shouldProceed) {
133+
console.log('Cancelled.')
134+
return
135+
}
136+
137+
const updatedState = {
138+
...state,
139+
active: false,
140+
completedAt: new Date().toISOString(),
141+
terminationReason: 'cancelled',
142+
}
143+
db.prepare('UPDATE project_kv SET data = ?, updated_at = ? WHERE project_id = ? AND key = ?').run(
144+
JSON.stringify(updatedState),
145+
Date.now(),
146+
loopToCancel.row.project_id,
147+
loopToCancel.row.key,
148+
)
149+
150+
console.log(`Cancelled Ralph loop: ${state.worktreeName}`)
151+
152+
if (argv.cleanup && state.worktreeDir && !state.inPlace) {
153+
if (existsSync(state.worktreeDir)) {
154+
try {
155+
const gitCommonDir = execSync('git rev-parse --git-common-dir', { cwd: state.worktreeDir, encoding: 'utf-8' }).trim()
156+
const gitRoot = resolve(state.worktreeDir, gitCommonDir, '..')
157+
const removeResult = spawnSync('git', ['worktree', 'remove', '-f', state.worktreeDir], { cwd: gitRoot, encoding: 'utf-8' })
158+
if (removeResult.status !== 0) {
159+
throw new Error(removeResult.stderr || 'git worktree remove failed')
160+
}
161+
console.log(`Removed worktree: ${state.worktreeDir}`)
162+
} catch {
163+
console.error(`Failed to remove worktree: ${state.worktreeDir}`)
164+
console.error('You may need to remove it manually.')
165+
}
166+
}
167+
}
168+
169+
console.log('')
175170
} finally {
176171
db.close()
177172
}
178173
}
179174

180-
async function runCancel(
181-
db: ReturnType<typeof openDatabase>,
182-
loopToCancel: { state: RalphState; row: { project_id: string; key: string; data: string } },
183-
options: CancelOptions & { worktreeName?: string },
184-
): Promise<void> {
185-
const { state } = loopToCancel
175+
export function help(): void {
176+
console.log(`
177+
Cancel a Ralph loop
186178
187-
const shouldProceed = options.force || await confirm(`Cancel Ralph loop '${state.worktreeName}'`)
179+
Usage:
180+
ocm-mem cancel [name] [options]
188181
189-
if (!shouldProceed) {
190-
console.log('Cancelled.')
191-
return
192-
}
182+
Arguments:
183+
name Worktree name to cancel (optional if only one active)
193184
194-
const updatedState = {
195-
...state,
196-
active: false,
197-
completedAt: new Date().toISOString(),
198-
terminationReason: 'cancelled',
199-
}
200-
db.prepare('UPDATE project_kv SET data = ?, updated_at = ? WHERE project_id = ? AND key = ?').run(
201-
JSON.stringify(updatedState),
202-
Date.now(),
203-
loopToCancel.row.project_id,
204-
loopToCancel.row.key,
205-
)
185+
Options:
186+
--cleanup Remove worktree directory after cancellation
187+
--force Skip confirmation prompt
188+
--project, -p <id> Project ID (auto-detected from git if not provided)
189+
--db-path <path> Path to memory database
190+
--help, -h Show this help message
191+
`.trim())
192+
}
206193

207-
console.log(`Cancelled Ralph loop: ${state.worktreeName}`)
194+
export async function cli(args: string[], globalOpts: { dbPath?: string; resolvedProjectId?: string }): Promise<void> {
195+
const argv: CancelArgs = {
196+
dbPath: globalOpts.dbPath,
197+
resolvedProjectId: globalOpts.resolvedProjectId,
198+
}
208199

209-
if (options.cleanup && state.worktreeDir && !state.inPlace) {
210-
if (existsSync(state.worktreeDir)) {
211-
try {
212-
const gitCommonDir = execSync('git rev-parse --git-common-dir', { cwd: state.worktreeDir, encoding: 'utf-8' }).trim()
213-
const gitRoot = resolve(state.worktreeDir, gitCommonDir, '..')
214-
const removeResult = spawnSync('git', ['worktree', 'remove', '-f', state.worktreeDir], { cwd: gitRoot, encoding: 'utf-8' })
215-
if (removeResult.status !== 0) {
216-
throw new Error(removeResult.stderr || 'git worktree remove failed')
217-
}
218-
console.log(`Removed worktree: ${state.worktreeDir}`)
219-
} catch {
220-
console.error(`Failed to remove worktree: ${state.worktreeDir}`)
221-
console.error('You may need to remove it manually.')
222-
}
200+
let i = 0
201+
while (i < args.length) {
202+
const arg = args[i]
203+
if (arg === '--cleanup') {
204+
argv.cleanup = true
205+
} else if (arg === '--force') {
206+
argv.force = true
207+
} else if (arg === '--help' || arg === '-h') {
208+
help()
209+
process.exit(0)
210+
} else if (!arg.startsWith('-')) {
211+
argv.name = arg
212+
} else {
213+
console.error(`Unknown option: ${arg}`)
214+
help()
215+
process.exit(1)
223216
}
217+
i++
224218
}
225219

226-
console.log('')
220+
await run(argv)
227221
}

0 commit comments

Comments
 (0)