@@ -3,79 +3,21 @@ import { openDatabase, confirm } from '../utils'
33import { execSync , spawnSync } from 'child_process'
44import { existsSync } from 'fs'
55import { 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