Skip to content

Commit c608ef5

Browse files
committed
feat: add CLI and config options for git operations
- Add autoCommit, autoPush, commitSignoff to Config interface - Add CLI flags: --no-auto-commit, --no-auto-push, -s/--signoff - Add environment variables: OPENCODER_AUTO_COMMIT, OPENCODER_AUTO_PUSH, OPENCODER_COMMIT_SIGNOFF - Support config.json options for git settings - Add CliOptions interface extensions - Add comprehensive config tests for new options Signed-off-by: leocavalcante <[email protected]>
1 parent d52e7b8 commit c608ef5

File tree

4 files changed

+597
-26
lines changed

4 files changed

+597
-26
lines changed

src/cli.ts

Lines changed: 118 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,23 @@ import { loadConfig } from "./config.ts"
77
import { runLoop } from "./loop.ts"
88
import type { CliOptions } from "./types.ts"
99

10-
const VERSION = "0.1.0"
10+
const VERSION = "1.0.0"
1111

1212
/**
13-
* Parse CLI arguments and run the application
13+
* Result of CLI argument parsing.
14+
* Contains the parsed options and optional hint argument.
1415
*/
15-
export async function run(): Promise<void> {
16+
export interface ParsedCli {
17+
/** Parsed CLI options (project, model, verbose, etc.) */
18+
options: CliOptions
19+
/** Optional hint/instruction for the AI, passed as a positional argument */
20+
hint?: string
21+
}
22+
23+
/**
24+
* Create and configure the CLI program
25+
*/
26+
function createProgram(): Command {
1627
const program = new Command()
1728

1829
program
@@ -25,23 +36,9 @@ export async function run(): Promise<void> {
2536
.option("-P, --plan-model <model>", "Model for plan phase (provider/model format)")
2637
.option("-B, --build-model <model>", "Model for build phase (provider/model format)")
2738
.option("-v, --verbose", "Enable verbose logging")
28-
.action(async (hint: string | undefined, opts: Record<string, unknown>) => {
29-
try {
30-
const cliOptions: CliOptions = {
31-
project: opts.project as string | undefined,
32-
model: opts.model as string | undefined,
33-
planModel: opts.planModel as string | undefined,
34-
buildModel: opts.buildModel as string | undefined,
35-
verbose: opts.verbose as boolean | undefined,
36-
}
37-
38-
const config = await loadConfig(cliOptions, hint)
39-
await runLoop(config)
40-
} catch (err) {
41-
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`)
42-
process.exit(1)
43-
}
44-
})
39+
.option("--no-auto-commit", "Disable automatic commits after tasks")
40+
.option("--no-auto-push", "Disable automatic push after cycles")
41+
.option("-s, --signoff", "Add Signed-off-by line to commits")
4542

4643
// Add examples to help
4744
program.addHelpText(
@@ -60,27 +57,127 @@ Examples:
6057
$ opencoder -m openai/gpt-4o -p ./myproject -v
6158
Run with verbose logging in a specific directory
6259
60+
$ opencoder -m anthropic/claude-sonnet-4 --no-auto-commit --no-auto-push
61+
Run without automatic git operations
62+
63+
$ opencoder -m anthropic/claude-sonnet-4 -s
64+
Run with commit signoff enabled
65+
6366
Options:
6467
-p, --project <dir> Project directory (default: current directory)
6568
-m, --model <model> Model for both plan and build
6669
-P, --plan-model Model for plan phase
6770
-B, --build-model Model for build phase
6871
-v, --verbose Enable verbose logging
72+
--no-auto-commit Disable automatic commits after tasks
73+
--no-auto-push Disable automatic push after cycles
74+
-s, --signoff Add Signed-off-by line to commits
6975
7076
Environment variables:
7177
OPENCODER_PLAN_MODEL Default plan model
7278
OPENCODER_BUILD_MODEL Default build model
7379
OPENCODER_VERBOSE Enable verbose logging (true/1)
7480
OPENCODER_PROJECT_DIR Default project directory
81+
OPENCODER_AUTO_COMMIT Enable auto-commit (true/1, default: true)
82+
OPENCODER_AUTO_PUSH Enable auto-push (true/1, default: true)
83+
OPENCODER_COMMIT_SIGNOFF Add signoff to commits (true/1, default: false)
7584
7685
Config file (.opencode/opencoder/config.json):
7786
{
7887
"planModel": "anthropic/claude-sonnet-4",
7988
"buildModel": "anthropic/claude-sonnet-4",
80-
"verbose": false
89+
"verbose": false,
90+
"autoCommit": true,
91+
"autoPush": true,
92+
"commitSignoff": false
8193
}
8294
`,
8395
)
8496

97+
return program
98+
}
99+
100+
/**
101+
* Parse CLI arguments without executing the action.
102+
* Useful for testing or when you need to inspect arguments before running.
103+
*
104+
* @param argv - Command line arguments array. Defaults to process.argv.
105+
* Should include the node executable and script name as first two elements.
106+
* @returns Parsed CLI options and hint
107+
*
108+
* @example
109+
* ```typescript
110+
* // Parse default process arguments
111+
* const { options, hint } = parseCli()
112+
*
113+
* // Parse custom arguments for testing
114+
* const { options, hint } = parseCli(['node', 'opencoder', '-m', 'anthropic/claude-sonnet-4', 'my hint'])
115+
* ```
116+
*/
117+
export function parseCli(argv: string[] = process.argv): ParsedCli {
118+
const program = createProgram()
119+
120+
// Parse without running the action
121+
program.parse(argv)
122+
123+
const opts = program.opts()
124+
const args = program.args
125+
126+
return {
127+
options: {
128+
project: opts.project as string | undefined,
129+
model: opts.model as string | undefined,
130+
planModel: opts.planModel as string | undefined,
131+
buildModel: opts.buildModel as string | undefined,
132+
verbose: opts.verbose as boolean | undefined,
133+
autoCommit: opts.autoCommit as boolean | undefined,
134+
autoPush: opts.autoPush as boolean | undefined,
135+
commitSignoff: opts.signoff as boolean | undefined,
136+
},
137+
hint: args[0],
138+
}
139+
}
140+
141+
/**
142+
* Parse CLI arguments and run the autonomous development loop.
143+
* This is the main entry point for the CLI application.
144+
*
145+
* Parses command line arguments, loads configuration from all sources
146+
* (defaults, config file, environment variables, CLI options), and
147+
* starts the autonomous development loop.
148+
*
149+
* @throws Will call process.exit(1) if configuration is invalid or an error occurs
150+
*
151+
* @example
152+
* ```typescript
153+
* // In your entry point (e.g., index.ts)
154+
* import { run } from './cli.ts'
155+
* await run()
156+
* ```
157+
*/
158+
export async function run(): Promise<void> {
159+
const program = createProgram()
160+
161+
program.action(async (hint: string | undefined, opts: Record<string, unknown>) => {
162+
try {
163+
const cliOptions: CliOptions = {
164+
project: opts.project as string | undefined,
165+
model: opts.model as string | undefined,
166+
planModel: opts.planModel as string | undefined,
167+
buildModel: opts.buildModel as string | undefined,
168+
verbose: opts.verbose as boolean | undefined,
169+
autoCommit: opts.autoCommit as boolean | undefined,
170+
autoPush: opts.autoPush as boolean | undefined,
171+
commitSignoff: opts.signoff as boolean | undefined,
172+
}
173+
174+
const config = await loadConfig(cliOptions, hint)
175+
await runLoop(config)
176+
} catch (err) {
177+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`)
178+
process.exit(1)
179+
}
180+
})
181+
85182
await program.parseAsync()
86183
}

src/config.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ const DEFAULTS: Omit<Config, "planModel" | "buildModel" | "projectDir"> = {
2020
backoffBase: 10,
2121
logRetention: 30,
2222
taskPauseSeconds: 2,
23+
autoCommit: true,
24+
autoPush: true,
25+
commitSignoff: false,
2326
}
2427

2528
/** Environment variable prefix */
@@ -43,6 +46,9 @@ export async function loadConfig(cliOptions: CliOptions, hint?: string): Promise
4346
planModel: cliOptions.planModel || cliOptions.model,
4447
buildModel: cliOptions.buildModel || cliOptions.model,
4548
verbose: cliOptions.verbose,
49+
autoCommit: cliOptions.autoCommit,
50+
autoPush: cliOptions.autoPush,
51+
commitSignoff: cliOptions.commitSignoff,
4652
}
4753

4854
// Merge all sources (later sources override earlier ones)
@@ -93,15 +99,19 @@ async function loadConfigFile(projectDir: string): Promise<Partial<Config>> {
9399
const content = await readFile(configPath, "utf-8")
94100
const parsed = JSON.parse(content) as ConfigFile
95101

96-
return {
102+
// Only include defined values to avoid overriding defaults with undefined
103+
return filterUndefined({
97104
planModel: parsed.planModel,
98105
buildModel: parsed.buildModel,
99106
verbose: parsed.verbose,
100107
maxRetries: parsed.maxRetries,
101108
backoffBase: parsed.backoffBase,
102109
logRetention: parsed.logRetention,
103110
taskPauseSeconds: parsed.taskPauseSeconds,
104-
}
111+
autoCommit: parsed.autoCommit,
112+
autoPush: parsed.autoPush,
113+
commitSignoff: parsed.commitSignoff,
114+
})
105115
} catch (err) {
106116
console.warn(`Warning: Failed to parse config.json: ${err}`)
107117
return {}
@@ -147,6 +157,15 @@ function loadEnvConfig(): Partial<Config> {
147157
if (!Number.isNaN(parsed)) config.taskPauseSeconds = parsed
148158
}
149159

160+
const autoCommit = process.env[`${ENV_PREFIX}AUTO_COMMIT`]
161+
if (autoCommit) config.autoCommit = autoCommit === "true" || autoCommit === "1"
162+
163+
const autoPush = process.env[`${ENV_PREFIX}AUTO_PUSH`]
164+
if (autoPush) config.autoPush = autoPush === "true" || autoPush === "1"
165+
166+
const commitSignoff = process.env[`${ENV_PREFIX}COMMIT_SIGNOFF`]
167+
if (commitSignoff) config.commitSignoff = commitSignoff === "true" || commitSignoff === "1"
168+
150169
return config
151170
}
152171

src/types.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Core type definitions for Opencoder
2+
* Core type definitions for OpenCoder
33
*/
44

55
/** Phases in the autonomous loop */
@@ -25,6 +25,12 @@ export interface Config {
2525
logRetention: number
2626
/** Seconds to pause between tasks */
2727
taskPauseSeconds: number
28+
/** Automatically commit changes after each task */
29+
autoCommit: boolean
30+
/** Automatically push commits after each cycle */
31+
autoPush: boolean
32+
/** Add signoff flag (-s) to commits */
33+
commitSignoff: boolean
2834
}
2935

3036
/** Persisted state */
@@ -39,6 +45,10 @@ export interface State {
3945
sessionId?: string
4046
/** ISO timestamp of last state update */
4147
lastUpdate: string
48+
/** Path to the idea file currently being processed (if any) */
49+
currentIdeaPath?: string
50+
/** Filename of the idea currently being processed (for display) */
51+
currentIdeaFilename?: string
4252
}
4353

4454
/** Runtime state with additional non-persisted fields */
@@ -118,6 +128,9 @@ export interface ConfigFile {
118128
backoffBase?: number
119129
logRetention?: number
120130
taskPauseSeconds?: number
131+
autoCommit?: boolean
132+
autoPush?: boolean
133+
commitSignoff?: boolean
121134
}
122135

123136
/** CLI options from argument parsing */
@@ -127,4 +140,7 @@ export interface CliOptions {
127140
planModel?: string
128141
buildModel?: string
129142
verbose?: boolean
143+
autoCommit?: boolean
144+
autoPush?: boolean
145+
commitSignoff?: boolean
130146
}

0 commit comments

Comments
 (0)