carson 3.19.0 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +11 -3
- data/RELEASE.md +25 -0
- data/VERSION +1 -1
- data/exe/carson +3 -3
- data/hooks/command-guard +56 -0
- data/hooks/pre-push +37 -1
- data/lib/carson/adapters/agent.rb +1 -0
- data/lib/carson/adapters/claude.rb +2 -0
- data/lib/carson/adapters/codex.rb +2 -0
- data/lib/carson/adapters/git.rb +2 -0
- data/lib/carson/adapters/github.rb +2 -0
- data/lib/carson/adapters/prompt.rb +2 -0
- data/lib/carson/cli.rb +415 -414
- data/lib/carson/config.rb +4 -3
- data/lib/carson/runtime/audit.rb +84 -84
- data/lib/carson/runtime/deliver.rb +27 -24
- data/lib/carson/runtime/govern.rb +29 -29
- data/lib/carson/runtime/housekeep.rb +15 -15
- data/lib/carson/runtime/local/hooks.rb +20 -0
- data/lib/carson/runtime/local/onboard.rb +17 -17
- data/lib/carson/runtime/local/prune.rb +13 -13
- data/lib/carson/runtime/local/sync.rb +6 -6
- data/lib/carson/runtime/local/template.rb +26 -25
- data/lib/carson/runtime/local/worktree.rb +76 -33
- data/lib/carson/runtime/local.rb +1 -0
- data/lib/carson/runtime/repos.rb +1 -1
- data/lib/carson/runtime/review/data_access.rb +1 -0
- data/lib/carson/runtime/review/gate_support.rb +15 -14
- data/lib/carson/runtime/review/query_text.rb +1 -0
- data/lib/carson/runtime/review/sweep_support.rb +5 -4
- data/lib/carson/runtime/review/utility.rb +2 -1
- data/lib/carson/runtime/review.rb +10 -8
- data/lib/carson/runtime/setup.rb +12 -10
- data/lib/carson/runtime/status.rb +20 -20
- data/lib/carson/runtime.rb +39 -25
- data/lib/carson/version.rb +1 -0
- data/lib/carson.rb +1 -0
- data/templates/.github/carson.md +7 -4
- metadata +2 -1
data/lib/carson/cli.rb
CHANGED
|
@@ -1,130 +1,131 @@
|
|
|
1
|
+
# Parses command-line arguments and dispatches to Runtime operations.
|
|
1
2
|
require "optparse"
|
|
2
3
|
|
|
3
4
|
module Carson
|
|
4
5
|
class CLI
|
|
5
|
-
def self.start(
|
|
6
|
-
parsed = parse_args(
|
|
6
|
+
def self.start( arguments:, repo_root:, tool_root:, output:, error: )
|
|
7
|
+
parsed = parse_args( arguments: arguments, output: output, error: error )
|
|
7
8
|
command = parsed.fetch( :command )
|
|
8
9
|
return Runtime::EXIT_OK if command == :help
|
|
9
10
|
|
|
10
11
|
if command == "version"
|
|
11
|
-
|
|
12
|
+
output.puts "#{BADGE} #{Carson::VERSION}"
|
|
12
13
|
return Runtime::EXIT_OK
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
if %w[repos refresh:all prune:all housekeep:all housekeep:target template:check:all audit:all sync:all status:all].include?( command )
|
|
16
17
|
verbose = parsed.fetch( :verbose, false )
|
|
17
|
-
runtime = Runtime.new( repo_root: repo_root, tool_root: tool_root,
|
|
18
|
+
runtime = Runtime.new( repo_root: repo_root, tool_root: tool_root, output: output, error: error, verbose: verbose )
|
|
18
19
|
return dispatch( parsed: parsed, runtime: runtime )
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
target_repo_root = parsed.fetch( :repo_root, nil )
|
|
22
23
|
target_repo_root = repo_root if target_repo_root.to_s.strip.empty?
|
|
23
24
|
unless Dir.exist?( target_repo_root )
|
|
24
|
-
|
|
25
|
+
error.puts "#{BADGE} ERROR: repository path does not exist: #{target_repo_root}"
|
|
25
26
|
return Runtime::EXIT_ERROR
|
|
26
27
|
end
|
|
27
28
|
|
|
28
29
|
verbose = parsed.fetch( :verbose, false )
|
|
29
|
-
runtime = Runtime.new( repo_root: target_repo_root, tool_root: tool_root,
|
|
30
|
+
runtime = Runtime.new( repo_root: target_repo_root, tool_root: tool_root, output: output, error: error, verbose: verbose )
|
|
30
31
|
dispatch( parsed: parsed, runtime: runtime )
|
|
31
|
-
rescue ConfigError =>
|
|
32
|
-
|
|
32
|
+
rescue ConfigError => exception
|
|
33
|
+
error.puts "#{BADGE} CONFIG ERROR: #{exception.message}"
|
|
33
34
|
Runtime::EXIT_ERROR
|
|
34
|
-
rescue StandardError =>
|
|
35
|
-
|
|
35
|
+
rescue StandardError => exception
|
|
36
|
+
error.puts "#{BADGE} ERROR: #{exception.message}"
|
|
36
37
|
Runtime::EXIT_ERROR
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
def self.parse_args(
|
|
40
|
-
verbose =
|
|
40
|
+
def self.parse_args( arguments:, output:, error: )
|
|
41
|
+
verbose = arguments.delete( "--verbose" ) ? true : false
|
|
41
42
|
parser = build_parser
|
|
42
|
-
preset = parse_preset_command(
|
|
43
|
+
preset = parse_preset_command( arguments: arguments, output: output, parser: parser )
|
|
43
44
|
return preset.merge( verbose: verbose ) unless preset.nil?
|
|
44
45
|
|
|
45
|
-
command =
|
|
46
|
-
result = parse_command( command: command,
|
|
46
|
+
command = arguments.shift
|
|
47
|
+
result = parse_command( command: command, arguments: arguments, error: error )
|
|
47
48
|
result.merge( verbose: verbose )
|
|
48
|
-
rescue OptionParser::ParseError =>
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
rescue OptionParser::ParseError => exception
|
|
50
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
51
|
+
error.puts parser
|
|
51
52
|
{ command: :invalid }
|
|
52
53
|
end
|
|
53
54
|
|
|
54
55
|
def self.build_parser
|
|
55
|
-
OptionParser.new do |
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
56
|
+
OptionParser.new do |parser|
|
|
57
|
+
parser.banner = "Usage: carson <command> [options]"
|
|
58
|
+
parser.separator ""
|
|
59
|
+
parser.separator "Repository governance and workflow automation for coding agents."
|
|
60
|
+
parser.separator ""
|
|
61
|
+
parser.separator "Commands:"
|
|
62
|
+
parser.separator " status Show repository state (branch, PRs, worktrees)"
|
|
63
|
+
parser.separator " setup Initialise Carson configuration"
|
|
64
|
+
parser.separator " audit Run pre-commit health checks"
|
|
65
|
+
parser.separator " sync Sync local main with remote"
|
|
66
|
+
parser.separator " deliver Push, create PR, and optionally merge"
|
|
67
|
+
parser.separator " prune Remove stale local branches"
|
|
68
|
+
parser.separator " worktree Manage isolated coding worktrees"
|
|
69
|
+
parser.separator " housekeep Sync, reap worktrees, and prune branches"
|
|
70
|
+
parser.separator " repos List governed repositories"
|
|
71
|
+
parser.separator " onboard Register a repository for governance"
|
|
72
|
+
parser.separator " offboard Remove a repository from governance"
|
|
73
|
+
parser.separator " refresh Re-install hooks and configuration"
|
|
74
|
+
parser.separator " template Manage canonical template files"
|
|
75
|
+
parser.separator " review Manage PR review workflow"
|
|
76
|
+
parser.separator " govern Portfolio-level PR triage loop"
|
|
77
|
+
parser.separator " version Show Carson version"
|
|
78
|
+
parser.separator ""
|
|
79
|
+
parser.separator "Run `carson <command> --help` for details on a specific command."
|
|
79
80
|
end
|
|
80
81
|
end
|
|
81
82
|
|
|
82
|
-
def self.parse_preset_command(
|
|
83
|
-
first =
|
|
83
|
+
def self.parse_preset_command( arguments:, output:, parser: )
|
|
84
|
+
first = arguments.first
|
|
84
85
|
if [ "--help", "-h" ].include?( first )
|
|
85
|
-
|
|
86
|
+
output.puts parser
|
|
86
87
|
return { command: :help }
|
|
87
88
|
end
|
|
88
89
|
return { command: "version" } if [ "--version", "-v" ].include?( first )
|
|
89
|
-
return { command: "audit" } if
|
|
90
|
+
return { command: "audit" } if arguments.empty?
|
|
90
91
|
|
|
91
92
|
nil
|
|
92
93
|
end
|
|
93
94
|
|
|
94
|
-
def self.parse_command( command:,
|
|
95
|
+
def self.parse_command( command:, arguments:, error: )
|
|
95
96
|
case command
|
|
96
97
|
when "version"
|
|
97
98
|
{ command: "version" }
|
|
98
99
|
when "setup"
|
|
99
|
-
parse_setup_command(
|
|
100
|
+
parse_setup_command( arguments: arguments, error: error )
|
|
100
101
|
when "onboard"
|
|
101
|
-
parse_onboard_command(
|
|
102
|
+
parse_onboard_command( arguments: arguments, error: error )
|
|
102
103
|
when "offboard"
|
|
103
|
-
parse_offboard_command(
|
|
104
|
+
parse_offboard_command( arguments: arguments, error: error )
|
|
104
105
|
when "refresh"
|
|
105
|
-
parse_refresh_command(
|
|
106
|
+
parse_refresh_command( arguments: arguments, error: error )
|
|
106
107
|
when "template"
|
|
107
|
-
parse_template_subcommand(
|
|
108
|
+
parse_template_subcommand( arguments: arguments, error: error )
|
|
108
109
|
when "prune"
|
|
109
|
-
parse_prune_command(
|
|
110
|
+
parse_prune_command( arguments: arguments, error: error )
|
|
110
111
|
when "worktree"
|
|
111
|
-
parse_worktree_subcommand(
|
|
112
|
+
parse_worktree_subcommand( arguments: arguments, error: error )
|
|
112
113
|
when "repos"
|
|
113
|
-
parse_repos_command(
|
|
114
|
+
parse_repos_command( arguments: arguments, error: error )
|
|
114
115
|
when "housekeep"
|
|
115
|
-
parse_housekeep_command(
|
|
116
|
+
parse_housekeep_command( arguments: arguments, error: error )
|
|
116
117
|
when "review"
|
|
117
|
-
parse_review_subcommand(
|
|
118
|
+
parse_review_subcommand( arguments: arguments, error: error )
|
|
118
119
|
when "audit"
|
|
119
|
-
parse_audit_command(
|
|
120
|
+
parse_audit_command( arguments: arguments, error: error )
|
|
120
121
|
when "sync"
|
|
121
|
-
parse_sync_command(
|
|
122
|
+
parse_sync_command( arguments: arguments, error: error )
|
|
122
123
|
when "status"
|
|
123
|
-
parse_status_command(
|
|
124
|
+
parse_status_command( arguments: arguments, error: error )
|
|
124
125
|
when "deliver"
|
|
125
|
-
parse_deliver_command(
|
|
126
|
+
parse_deliver_command( arguments: arguments, error: error )
|
|
126
127
|
when "govern"
|
|
127
|
-
parse_govern_subcommand(
|
|
128
|
+
parse_govern_subcommand( arguments: arguments, error: error )
|
|
128
129
|
else
|
|
129
130
|
{ command: command }
|
|
130
131
|
end
|
|
@@ -132,428 +133,428 @@ module Carson
|
|
|
132
133
|
|
|
133
134
|
# --- setup ---
|
|
134
135
|
|
|
135
|
-
def self.parse_setup_command(
|
|
136
|
+
def self.parse_setup_command( arguments:, error: )
|
|
136
137
|
options = {}
|
|
137
|
-
setup_parser = OptionParser.new do |
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
end
|
|
156
|
-
setup_parser.parse!(
|
|
157
|
-
unless
|
|
158
|
-
|
|
159
|
-
|
|
138
|
+
setup_parser = OptionParser.new do |parser|
|
|
139
|
+
parser.banner = "Usage: carson setup [--remote NAME] [--main-branch NAME] [--workflow STYLE] [--merge METHOD] [--canonical PATH]"
|
|
140
|
+
parser.separator ""
|
|
141
|
+
parser.separator "Initialise Carson configuration for the current repository."
|
|
142
|
+
parser.separator "Detects git remote, main branch, and workflow style, then writes .carson.yml."
|
|
143
|
+
parser.separator "Pass flags to override detected values."
|
|
144
|
+
parser.separator ""
|
|
145
|
+
parser.separator "Options:"
|
|
146
|
+
parser.on( "--remote NAME", "Git remote name" ) { |value| options[ "git.remote" ] = value }
|
|
147
|
+
parser.on( "--main-branch NAME", "Main branch name" ) { |value| options[ "git.main_branch" ] = value }
|
|
148
|
+
parser.on( "--workflow STYLE", "Workflow style (branch or trunk)" ) { |value| options[ "workflow.style" ] = value }
|
|
149
|
+
parser.on( "--merge METHOD", "Merge method (squash, rebase, or merge)" ) { |value| options[ "govern.merge.method" ] = value }
|
|
150
|
+
parser.on( "--canonical PATH", "Canonical template directory path" ) { |value| options[ "template.canonical" ] = value }
|
|
151
|
+
parser.separator ""
|
|
152
|
+
parser.separator "Examples:"
|
|
153
|
+
parser.separator " carson setup Auto-detect and write config"
|
|
154
|
+
parser.separator " carson setup --remote github Use 'github' as the git remote"
|
|
155
|
+
parser.separator " carson setup --merge squash Set squash as the merge method"
|
|
156
|
+
end
|
|
157
|
+
setup_parser.parse!( arguments )
|
|
158
|
+
unless arguments.empty?
|
|
159
|
+
error.puts "#{BADGE} Unexpected arguments for setup: #{arguments.join( ' ' )}"
|
|
160
|
+
error.puts setup_parser
|
|
160
161
|
return { command: :invalid }
|
|
161
162
|
end
|
|
162
163
|
{ command: "setup", cli_choices: options }
|
|
163
|
-
rescue OptionParser::ParseError =>
|
|
164
|
-
|
|
164
|
+
rescue OptionParser::ParseError => exception
|
|
165
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
165
166
|
{ command: :invalid }
|
|
166
167
|
end
|
|
167
168
|
|
|
168
169
|
# --- onboard / offboard ---
|
|
169
170
|
|
|
170
|
-
def self.parse_onboard_command(
|
|
171
|
-
onboard_parser = OptionParser.new do |
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
end
|
|
182
|
-
onboard_parser.parse!(
|
|
183
|
-
if
|
|
184
|
-
|
|
185
|
-
|
|
171
|
+
def self.parse_onboard_command( arguments:, error: )
|
|
172
|
+
onboard_parser = OptionParser.new do |parser|
|
|
173
|
+
parser.banner = "Usage: carson onboard [REPO_PATH]"
|
|
174
|
+
parser.separator ""
|
|
175
|
+
parser.separator "Register a repository for Carson governance."
|
|
176
|
+
parser.separator "Detects the remote, installs hooks, applies templates, and runs initial audit."
|
|
177
|
+
parser.separator "Defaults to the current directory if no path is given."
|
|
178
|
+
parser.separator ""
|
|
179
|
+
parser.separator "Examples:"
|
|
180
|
+
parser.separator " carson onboard Onboard the current repository"
|
|
181
|
+
parser.separator " carson onboard ~/Dev/app Onboard a specific repository"
|
|
182
|
+
end
|
|
183
|
+
onboard_parser.parse!( arguments )
|
|
184
|
+
if arguments.length > 1
|
|
185
|
+
error.puts "#{BADGE} Too many arguments for onboard. Use: carson onboard [repo_path]"
|
|
186
|
+
error.puts onboard_parser
|
|
186
187
|
return { command: :invalid }
|
|
187
188
|
end
|
|
188
|
-
repo_path =
|
|
189
|
+
repo_path = arguments.first
|
|
189
190
|
{
|
|
190
191
|
command: "onboard",
|
|
191
192
|
repo_root: repo_path.to_s.strip.empty? ? nil : File.expand_path( repo_path )
|
|
192
193
|
}
|
|
193
|
-
rescue OptionParser::ParseError =>
|
|
194
|
-
|
|
194
|
+
rescue OptionParser::ParseError => exception
|
|
195
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
195
196
|
{ command: :invalid }
|
|
196
197
|
end
|
|
197
198
|
|
|
198
|
-
def self.parse_offboard_command(
|
|
199
|
-
offboard_parser = OptionParser.new do |
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
end
|
|
209
|
-
offboard_parser.parse!(
|
|
210
|
-
if
|
|
211
|
-
|
|
212
|
-
|
|
199
|
+
def self.parse_offboard_command( arguments:, error: )
|
|
200
|
+
offboard_parser = OptionParser.new do |parser|
|
|
201
|
+
parser.banner = "Usage: carson offboard [REPO_PATH]"
|
|
202
|
+
parser.separator ""
|
|
203
|
+
parser.separator "Remove a repository from Carson governance."
|
|
204
|
+
parser.separator "Unregisters the repo from Carson's portfolio and removes hooks."
|
|
205
|
+
parser.separator "Defaults to the current directory if no path is given."
|
|
206
|
+
parser.separator ""
|
|
207
|
+
parser.separator "Examples:"
|
|
208
|
+
parser.separator " carson offboard Offboard the current repository"
|
|
209
|
+
end
|
|
210
|
+
offboard_parser.parse!( arguments )
|
|
211
|
+
if arguments.length > 1
|
|
212
|
+
error.puts "#{BADGE} Too many arguments for offboard. Use: carson offboard [repo_path]"
|
|
213
|
+
error.puts offboard_parser
|
|
213
214
|
return { command: :invalid }
|
|
214
215
|
end
|
|
215
|
-
repo_path =
|
|
216
|
+
repo_path = arguments.first
|
|
216
217
|
{
|
|
217
218
|
command: "offboard",
|
|
218
219
|
repo_root: repo_path.to_s.strip.empty? ? nil : File.expand_path( repo_path )
|
|
219
220
|
}
|
|
220
|
-
rescue OptionParser::ParseError =>
|
|
221
|
-
|
|
221
|
+
rescue OptionParser::ParseError => exception
|
|
222
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
222
223
|
{ command: :invalid }
|
|
223
224
|
end
|
|
224
225
|
|
|
225
226
|
# --- refresh ---
|
|
226
227
|
|
|
227
|
-
def self.parse_refresh_command(
|
|
228
|
+
def self.parse_refresh_command( arguments:, error: )
|
|
228
229
|
options = { all: false }
|
|
229
|
-
refresh_parser = OptionParser.new do |
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
end
|
|
242
|
-
refresh_parser.parse!(
|
|
243
|
-
|
|
244
|
-
if options[ :all ] && !
|
|
245
|
-
|
|
246
|
-
|
|
230
|
+
refresh_parser = OptionParser.new do |parser|
|
|
231
|
+
parser.banner = "Usage: carson refresh [--all] [REPO_PATH]"
|
|
232
|
+
parser.separator ""
|
|
233
|
+
parser.separator "Re-install Carson hooks and configuration for a repository."
|
|
234
|
+
parser.separator "Defaults to the current directory. Use --all to refresh all governed repos."
|
|
235
|
+
parser.separator ""
|
|
236
|
+
parser.separator "Options:"
|
|
237
|
+
parser.on( "--all", "Refresh all governed repositories" ) { options[ :all ] = true }
|
|
238
|
+
parser.separator ""
|
|
239
|
+
parser.separator "Examples:"
|
|
240
|
+
parser.separator " carson refresh Refresh the current repository"
|
|
241
|
+
parser.separator " carson refresh --all Refresh all governed repos"
|
|
242
|
+
end
|
|
243
|
+
refresh_parser.parse!( arguments )
|
|
244
|
+
|
|
245
|
+
if options[ :all ] && !arguments.empty?
|
|
246
|
+
error.puts "#{BADGE} --all and repo_path are mutually exclusive. Use: carson refresh --all OR carson refresh [repo_path]"
|
|
247
|
+
error.puts refresh_parser
|
|
247
248
|
return { command: :invalid }
|
|
248
249
|
end
|
|
249
250
|
|
|
250
251
|
return { command: "refresh:all" } if options[ :all ]
|
|
251
252
|
|
|
252
|
-
if
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
if arguments.length > 1
|
|
254
|
+
error.puts "#{BADGE} Too many arguments for refresh. Use: carson refresh [repo_path]"
|
|
255
|
+
error.puts refresh_parser
|
|
255
256
|
return { command: :invalid }
|
|
256
257
|
end
|
|
257
258
|
|
|
258
|
-
repo_path =
|
|
259
|
+
repo_path = arguments.first
|
|
259
260
|
{
|
|
260
261
|
command: "refresh",
|
|
261
262
|
repo_root: repo_path.to_s.strip.empty? ? nil : File.expand_path( repo_path )
|
|
262
263
|
}
|
|
263
|
-
rescue OptionParser::ParseError =>
|
|
264
|
-
|
|
264
|
+
rescue OptionParser::ParseError => exception
|
|
265
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
265
266
|
{ command: :invalid }
|
|
266
267
|
end
|
|
267
268
|
|
|
268
269
|
# --- prune ---
|
|
269
270
|
|
|
270
|
-
def self.parse_prune_command(
|
|
271
|
+
def self.parse_prune_command( arguments:, error: )
|
|
271
272
|
options = { all: false, json: false }
|
|
272
|
-
prune_parser = OptionParser.new do |
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
end
|
|
287
|
-
prune_parser.parse!(
|
|
273
|
+
prune_parser = OptionParser.new do |parser|
|
|
274
|
+
parser.banner = "Usage: carson prune [--all] [--json]"
|
|
275
|
+
parser.separator ""
|
|
276
|
+
parser.separator "Remove stale local branches."
|
|
277
|
+
parser.separator "Cleans up branches gone from the remote, orphan branches with merged PRs,"
|
|
278
|
+
parser.separator "and absorbed branches whose content is already on main."
|
|
279
|
+
parser.separator ""
|
|
280
|
+
parser.separator "Options:"
|
|
281
|
+
parser.on( "--all", "Prune all governed repositories" ) { options[ :all ] = true }
|
|
282
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
283
|
+
parser.separator ""
|
|
284
|
+
parser.separator "Examples:"
|
|
285
|
+
parser.separator " carson prune Clean up stale branches in this repo"
|
|
286
|
+
parser.separator " carson prune --all Clean up across all governed repos"
|
|
287
|
+
end
|
|
288
|
+
prune_parser.parse!( arguments )
|
|
288
289
|
return { command: "prune:all", json: options[ :json ] } if options[ :all ]
|
|
289
290
|
{ command: "prune", json: options[ :json ] }
|
|
290
|
-
rescue OptionParser::ParseError =>
|
|
291
|
-
|
|
291
|
+
rescue OptionParser::ParseError => exception
|
|
292
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
292
293
|
{ command: :invalid }
|
|
293
294
|
end
|
|
294
295
|
|
|
295
296
|
# --- worktree ---
|
|
296
297
|
|
|
297
|
-
def self.parse_worktree_subcommand(
|
|
298
|
+
def self.parse_worktree_subcommand( arguments:, error: )
|
|
298
299
|
options = { json: false, force: false }
|
|
299
|
-
worktree_parser = OptionParser.new do |
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
end
|
|
318
|
-
worktree_parser.parse!(
|
|
319
|
-
|
|
320
|
-
action =
|
|
300
|
+
worktree_parser = OptionParser.new do |parser|
|
|
301
|
+
parser.banner = "Usage: carson worktree <create|remove> <name> [options]"
|
|
302
|
+
parser.separator ""
|
|
303
|
+
parser.separator "Manage isolated worktrees for coding agents."
|
|
304
|
+
parser.separator "Create auto-syncs main before branching. Remove guards against"
|
|
305
|
+
parser.separator "unpushed commits and CWD-inside-worktree by default."
|
|
306
|
+
parser.separator ""
|
|
307
|
+
parser.separator "Subcommands:"
|
|
308
|
+
parser.separator " create <name> Create a new worktree with a fresh branch"
|
|
309
|
+
parser.separator " remove <name> [--force] Remove a worktree (--force skips safety checks)"
|
|
310
|
+
parser.separator ""
|
|
311
|
+
parser.separator "Options:"
|
|
312
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
313
|
+
parser.on( "--force", "Skip safety checks on remove" ) { options[ :force ] = true }
|
|
314
|
+
parser.separator ""
|
|
315
|
+
parser.separator "Examples:"
|
|
316
|
+
parser.separator " carson worktree create feature-x Create an isolated worktree"
|
|
317
|
+
parser.separator " carson worktree remove feature-x Remove after work is pushed"
|
|
318
|
+
end
|
|
319
|
+
worktree_parser.parse!( arguments )
|
|
320
|
+
|
|
321
|
+
action = arguments.shift
|
|
321
322
|
if action.to_s.strip.empty?
|
|
322
|
-
|
|
323
|
-
|
|
323
|
+
error.puts "#{BADGE} Missing subcommand for worktree. Use: carson worktree create|remove <name>"
|
|
324
|
+
error.puts worktree_parser
|
|
324
325
|
return { command: :invalid }
|
|
325
326
|
end
|
|
326
327
|
|
|
327
328
|
case action
|
|
328
329
|
when "create"
|
|
329
|
-
name =
|
|
330
|
+
name = arguments.shift
|
|
330
331
|
if name.to_s.strip.empty?
|
|
331
|
-
|
|
332
|
+
error.puts "#{BADGE} Missing name for worktree create. Use: carson worktree create <name>"
|
|
332
333
|
return { command: :invalid }
|
|
333
334
|
end
|
|
334
335
|
{ command: "worktree:create", worktree_name: name, json: options[ :json ] }
|
|
335
336
|
when "remove"
|
|
336
|
-
worktree_path =
|
|
337
|
+
worktree_path = arguments.shift
|
|
337
338
|
if worktree_path.to_s.strip.empty?
|
|
338
|
-
|
|
339
|
+
error.puts "#{BADGE} Missing path for worktree remove. Use: carson worktree remove <name-or-path>"
|
|
339
340
|
return { command: :invalid }
|
|
340
341
|
end
|
|
341
342
|
{ command: "worktree:remove", worktree_path: worktree_path, force: options[ :force ], json: options[ :json ] }
|
|
342
343
|
else
|
|
343
|
-
|
|
344
|
+
error.puts "#{BADGE} Unknown worktree subcommand: #{action}. Use: carson worktree create|remove <name>"
|
|
344
345
|
{ command: :invalid }
|
|
345
346
|
end
|
|
346
|
-
rescue OptionParser::ParseError =>
|
|
347
|
-
|
|
347
|
+
rescue OptionParser::ParseError => exception
|
|
348
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
348
349
|
{ command: :invalid }
|
|
349
350
|
end
|
|
350
351
|
|
|
351
352
|
# --- review ---
|
|
352
353
|
|
|
353
|
-
def self.parse_review_subcommand(
|
|
354
|
-
review_parser = OptionParser.new do |
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
end
|
|
367
|
-
review_parser.parse!(
|
|
368
|
-
|
|
369
|
-
action =
|
|
354
|
+
def self.parse_review_subcommand( arguments:, error: )
|
|
355
|
+
review_parser = OptionParser.new do |parser|
|
|
356
|
+
parser.banner = "Usage: carson review <gate|sweep>"
|
|
357
|
+
parser.separator ""
|
|
358
|
+
parser.separator "Manage PR review workflow."
|
|
359
|
+
parser.separator ""
|
|
360
|
+
parser.separator "Subcommands:"
|
|
361
|
+
parser.separator " gate Check if review requirements are met for merge"
|
|
362
|
+
parser.separator " sweep Scan and resolve pending review threads"
|
|
363
|
+
parser.separator ""
|
|
364
|
+
parser.separator "Examples:"
|
|
365
|
+
parser.separator " carson review gate Check merge readiness"
|
|
366
|
+
parser.separator " carson review sweep Resolve pending review threads"
|
|
367
|
+
end
|
|
368
|
+
review_parser.parse!( arguments )
|
|
369
|
+
|
|
370
|
+
action = arguments.shift
|
|
370
371
|
if action.to_s.strip.empty?
|
|
371
|
-
|
|
372
|
-
|
|
372
|
+
error.puts "#{BADGE} Missing subcommand for review. Use: carson review gate|sweep"
|
|
373
|
+
error.puts review_parser
|
|
373
374
|
return { command: :invalid }
|
|
374
375
|
end
|
|
375
376
|
{ command: "review:#{action}" }
|
|
376
|
-
rescue OptionParser::ParseError =>
|
|
377
|
-
|
|
377
|
+
rescue OptionParser::ParseError => exception
|
|
378
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
378
379
|
{ command: :invalid }
|
|
379
380
|
end
|
|
380
381
|
|
|
381
382
|
# --- template ---
|
|
382
383
|
|
|
383
|
-
def self.parse_template_subcommand(
|
|
384
|
+
def self.parse_template_subcommand( arguments:, error: )
|
|
384
385
|
# Handle parent-level help or missing subcommand.
|
|
385
|
-
if
|
|
386
|
-
template_parser = OptionParser.new do |
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
386
|
+
if arguments.empty? || [ "--help", "-h" ].include?( arguments.first )
|
|
387
|
+
template_parser = OptionParser.new do |parser|
|
|
388
|
+
parser.banner = "Usage: carson template <check|apply> [options]"
|
|
389
|
+
parser.separator ""
|
|
390
|
+
parser.separator "Manage canonical template files (CI workflows, lint configs)."
|
|
391
|
+
parser.separator ""
|
|
392
|
+
parser.separator "Subcommands:"
|
|
393
|
+
parser.separator " check Show template drift without making changes"
|
|
394
|
+
parser.separator " apply [--push-prep] Sync templates into the repository"
|
|
395
|
+
parser.separator ""
|
|
396
|
+
parser.separator "Examples:"
|
|
397
|
+
parser.separator " carson template check Check for template drift"
|
|
398
|
+
parser.separator " carson template apply Apply canonical templates"
|
|
398
399
|
end
|
|
399
400
|
|
|
400
|
-
if
|
|
401
|
-
|
|
402
|
-
|
|
401
|
+
if arguments.empty?
|
|
402
|
+
error.puts "#{BADGE} Missing subcommand for template. Use: carson template check|apply"
|
|
403
|
+
error.puts template_parser
|
|
403
404
|
return { command: :invalid }
|
|
404
405
|
end
|
|
405
406
|
|
|
406
407
|
# Let OptionParser handle --help (prints and exits).
|
|
407
|
-
template_parser.parse!(
|
|
408
|
+
template_parser.parse!( arguments )
|
|
408
409
|
return { command: :help }
|
|
409
410
|
end
|
|
410
411
|
|
|
411
|
-
action =
|
|
412
|
-
return { command: "template:check:all" } if action == "check" &&
|
|
412
|
+
action = arguments.shift
|
|
413
|
+
return { command: "template:check:all" } if action == "check" && arguments.include?( "--all" )
|
|
413
414
|
return { command: "template:#{action}" } unless action == "apply"
|
|
414
415
|
|
|
415
416
|
options = { push_prep: false }
|
|
416
|
-
apply_parser = OptionParser.new do |
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
417
|
+
apply_parser = OptionParser.new do |parser|
|
|
418
|
+
parser.banner = "Usage: carson template apply [--push-prep]"
|
|
419
|
+
parser.separator ""
|
|
420
|
+
parser.separator "Sync canonical template files (CI workflows, lint configs) into the repository."
|
|
421
|
+
parser.separator "Copies managed files from the configured canonical directory."
|
|
422
|
+
parser.separator ""
|
|
423
|
+
parser.separator "Options:"
|
|
424
|
+
parser.on( "--push-prep", "Apply templates and auto-commit any managed file changes (used by pre-push hook)" ) do
|
|
424
425
|
options[ :push_prep ] = true
|
|
425
426
|
end
|
|
426
427
|
end
|
|
427
|
-
apply_parser.parse!(
|
|
428
|
-
unless
|
|
429
|
-
|
|
430
|
-
|
|
428
|
+
apply_parser.parse!( arguments )
|
|
429
|
+
unless arguments.empty?
|
|
430
|
+
error.puts "#{BADGE} Unexpected arguments for template apply: #{arguments.join( ' ' )}"
|
|
431
|
+
error.puts apply_parser
|
|
431
432
|
return { command: :invalid }
|
|
432
433
|
end
|
|
433
434
|
{ command: "template:apply", push_prep: options.fetch( :push_prep ) }
|
|
434
|
-
rescue OptionParser::ParseError =>
|
|
435
|
-
|
|
435
|
+
rescue OptionParser::ParseError => exception
|
|
436
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
436
437
|
{ command: :invalid }
|
|
437
438
|
end
|
|
438
439
|
|
|
439
440
|
# --- audit ---
|
|
440
441
|
|
|
441
|
-
def self.parse_audit_command(
|
|
442
|
+
def self.parse_audit_command( arguments:, error: )
|
|
442
443
|
options = { json: false, all: false }
|
|
443
|
-
audit_parser = OptionParser.new do |
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
end
|
|
459
|
-
audit_parser.parse!(
|
|
460
|
-
unless
|
|
461
|
-
|
|
444
|
+
audit_parser = OptionParser.new do |parser|
|
|
445
|
+
parser.banner = "Usage: carson audit [--all] [--json]"
|
|
446
|
+
parser.separator ""
|
|
447
|
+
parser.separator "Run pre-commit health checks on the repository."
|
|
448
|
+
parser.separator "Validates hooks, main-branch sync, PR status, and CI baseline."
|
|
449
|
+
parser.separator "Exits with a non-zero status when policy violations are found."
|
|
450
|
+
parser.separator ""
|
|
451
|
+
parser.separator "Options:"
|
|
452
|
+
parser.on( "--all", "Audit all governed repositories" ) { options[ :all ] = true }
|
|
453
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
454
|
+
parser.separator ""
|
|
455
|
+
parser.separator "Examples:"
|
|
456
|
+
parser.separator " carson audit Check repository health (also the default command)"
|
|
457
|
+
parser.separator " carson audit --json Structured output for agent consumption"
|
|
458
|
+
parser.separator " carson audit --all Audit all governed repos"
|
|
459
|
+
end
|
|
460
|
+
audit_parser.parse!( arguments )
|
|
461
|
+
unless arguments.empty?
|
|
462
|
+
error.puts "#{BADGE} Unexpected arguments for audit: #{arguments.join( ' ' )}"
|
|
462
463
|
return { command: :invalid }
|
|
463
464
|
end
|
|
464
465
|
return { command: "audit:all" } if options[ :all ]
|
|
465
466
|
{ command: "audit", json: options[ :json ] }
|
|
466
|
-
rescue OptionParser::ParseError =>
|
|
467
|
-
|
|
467
|
+
rescue OptionParser::ParseError => exception
|
|
468
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
468
469
|
{ command: :invalid }
|
|
469
470
|
end
|
|
470
471
|
|
|
471
472
|
# --- sync ---
|
|
472
473
|
|
|
473
|
-
def self.parse_sync_command(
|
|
474
|
+
def self.parse_sync_command( arguments:, error: )
|
|
474
475
|
options = { json: false, all: false }
|
|
475
|
-
sync_parser = OptionParser.new do |
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
end
|
|
490
|
-
sync_parser.parse!(
|
|
491
|
-
unless
|
|
492
|
-
|
|
476
|
+
sync_parser = OptionParser.new do |parser|
|
|
477
|
+
parser.banner = "Usage: carson sync [--all] [--json]"
|
|
478
|
+
parser.separator ""
|
|
479
|
+
parser.separator "Sync the local main branch with the remote."
|
|
480
|
+
parser.separator "Fetches and fast-forwards main without switching branches."
|
|
481
|
+
parser.separator ""
|
|
482
|
+
parser.separator "Options:"
|
|
483
|
+
parser.on( "--all", "Sync all governed repositories" ) { options[ :all ] = true }
|
|
484
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
485
|
+
parser.separator ""
|
|
486
|
+
parser.separator "Examples:"
|
|
487
|
+
parser.separator " carson sync Pull latest changes from remote main"
|
|
488
|
+
parser.separator " carson sync --json Structured output for agent consumption"
|
|
489
|
+
parser.separator " carson sync --all Sync all governed repos"
|
|
490
|
+
end
|
|
491
|
+
sync_parser.parse!( arguments )
|
|
492
|
+
unless arguments.empty?
|
|
493
|
+
error.puts "#{BADGE} Unexpected arguments for sync: #{arguments.join( ' ' )}"
|
|
493
494
|
return { command: :invalid }
|
|
494
495
|
end
|
|
495
496
|
return { command: "sync:all" } if options[ :all ]
|
|
496
497
|
{ command: "sync", json: options[ :json ] }
|
|
497
|
-
rescue OptionParser::ParseError =>
|
|
498
|
-
|
|
498
|
+
rescue OptionParser::ParseError => exception
|
|
499
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
499
500
|
{ command: :invalid }
|
|
500
501
|
end
|
|
501
502
|
|
|
502
503
|
# --- status ---
|
|
503
504
|
|
|
504
|
-
def self.parse_status_command(
|
|
505
|
+
def self.parse_status_command( arguments:, error: )
|
|
505
506
|
options = { json: false, all: false }
|
|
506
|
-
status_parser = OptionParser.new do |
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
end
|
|
521
|
-
status_parser.parse!(
|
|
522
|
-
unless
|
|
523
|
-
|
|
507
|
+
status_parser = OptionParser.new do |parser|
|
|
508
|
+
parser.banner = "Usage: carson status [--all] [--json]"
|
|
509
|
+
parser.separator ""
|
|
510
|
+
parser.separator "Show the current state of the repository."
|
|
511
|
+
parser.separator "Reports branch, worktrees, open PRs, stale branches, and version."
|
|
512
|
+
parser.separator ""
|
|
513
|
+
parser.separator "Options:"
|
|
514
|
+
parser.on( "--all", "Show status for all governed repositories" ) { options[ :all ] = true }
|
|
515
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
516
|
+
parser.separator ""
|
|
517
|
+
parser.separator "Examples:"
|
|
518
|
+
parser.separator " carson status Quick overview of repository state"
|
|
519
|
+
parser.separator " carson status --json Structured output for agent consumption"
|
|
520
|
+
parser.separator " carson status --all Portfolio-wide status overview"
|
|
521
|
+
end
|
|
522
|
+
status_parser.parse!( arguments )
|
|
523
|
+
unless arguments.empty?
|
|
524
|
+
error.puts "#{BADGE} Unexpected arguments for status: #{arguments.join( ' ' )}"
|
|
524
525
|
return { command: :invalid }
|
|
525
526
|
end
|
|
526
527
|
return { command: "status:all", json: options[ :json ] } if options[ :all ]
|
|
527
528
|
{ command: "status", json: options[ :json ] }
|
|
528
|
-
rescue OptionParser::ParseError =>
|
|
529
|
-
|
|
529
|
+
rescue OptionParser::ParseError => exception
|
|
530
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
530
531
|
{ command: :invalid }
|
|
531
532
|
end
|
|
532
533
|
|
|
533
534
|
# --- deliver ---
|
|
534
535
|
|
|
535
|
-
def self.parse_deliver_command(
|
|
536
|
+
def self.parse_deliver_command( arguments:, error: )
|
|
536
537
|
options = { merge: false, json: false, title: nil, body_file: nil }
|
|
537
|
-
deliver_parser = OptionParser.new do |
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
end
|
|
553
|
-
deliver_parser.parse!(
|
|
554
|
-
unless
|
|
555
|
-
|
|
556
|
-
|
|
538
|
+
deliver_parser = OptionParser.new do |parser|
|
|
539
|
+
parser.banner = "Usage: carson deliver [--merge] [--json] [--title TITLE] [--body-file PATH]"
|
|
540
|
+
parser.separator ""
|
|
541
|
+
parser.separator "Push the current branch, create a pull request, and optionally merge."
|
|
542
|
+
parser.separator "Collapses the manual push → PR → merge flow into a single command."
|
|
543
|
+
parser.separator ""
|
|
544
|
+
parser.separator "Options:"
|
|
545
|
+
parser.on( "--merge", "Also merge the PR if CI passes" ) { options[ :merge ] = true }
|
|
546
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
547
|
+
parser.on( "--title TITLE", "PR title (defaults to branch name)" ) { |value| options[ :title ] = value }
|
|
548
|
+
parser.on( "--body-file PATH", "File containing PR body text" ) { |value| options[ :body_file ] = value }
|
|
549
|
+
parser.separator ""
|
|
550
|
+
parser.separator "Examples:"
|
|
551
|
+
parser.separator " carson deliver Push and open a PR"
|
|
552
|
+
parser.separator " carson deliver --merge Push, open a PR, and merge if CI passes"
|
|
553
|
+
end
|
|
554
|
+
deliver_parser.parse!( arguments )
|
|
555
|
+
unless arguments.empty?
|
|
556
|
+
error.puts "#{BADGE} Unexpected arguments for deliver: #{arguments.join( ' ' )}"
|
|
557
|
+
error.puts deliver_parser
|
|
557
558
|
return { command: :invalid }
|
|
558
559
|
end
|
|
559
560
|
{
|
|
@@ -563,113 +564,113 @@ module Carson
|
|
|
563
564
|
title: options[ :title ],
|
|
564
565
|
body_file: options[ :body_file ]
|
|
565
566
|
}
|
|
566
|
-
rescue OptionParser::ParseError =>
|
|
567
|
-
|
|
567
|
+
rescue OptionParser::ParseError => exception
|
|
568
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
568
569
|
{ command: :invalid }
|
|
569
570
|
end
|
|
570
571
|
|
|
571
572
|
# --- repos ---
|
|
572
573
|
|
|
573
|
-
def self.parse_repos_command(
|
|
574
|
+
def self.parse_repos_command( arguments:, error: )
|
|
574
575
|
options = { json: false }
|
|
575
|
-
repos_parser = OptionParser.new do |
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
end
|
|
588
|
-
repos_parser.parse!(
|
|
589
|
-
unless
|
|
590
|
-
|
|
576
|
+
repos_parser = OptionParser.new do |parser|
|
|
577
|
+
parser.banner = "Usage: carson repos [--json]"
|
|
578
|
+
parser.separator ""
|
|
579
|
+
parser.separator "List all repositories governed by Carson."
|
|
580
|
+
parser.separator "Shows the portfolio of repos registered via carson onboard."
|
|
581
|
+
parser.separator ""
|
|
582
|
+
parser.separator "Options:"
|
|
583
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
584
|
+
parser.separator ""
|
|
585
|
+
parser.separator "Examples:"
|
|
586
|
+
parser.separator " carson repos List governed repositories"
|
|
587
|
+
parser.separator " carson repos --json Structured output for agent consumption"
|
|
588
|
+
end
|
|
589
|
+
repos_parser.parse!( arguments )
|
|
590
|
+
unless arguments.empty?
|
|
591
|
+
error.puts "#{BADGE} Unexpected arguments for repos: #{arguments.join( ' ' )}"
|
|
591
592
|
return { command: :invalid }
|
|
592
593
|
end
|
|
593
594
|
{ command: "repos", json: options[ :json ] }
|
|
594
|
-
rescue OptionParser::ParseError =>
|
|
595
|
-
|
|
595
|
+
rescue OptionParser::ParseError => exception
|
|
596
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
596
597
|
{ command: :invalid }
|
|
597
598
|
end
|
|
598
599
|
|
|
599
600
|
# --- housekeep ---
|
|
600
601
|
|
|
601
|
-
def self.parse_housekeep_command(
|
|
602
|
+
def self.parse_housekeep_command( arguments:, error: )
|
|
602
603
|
options = { all: false, json: false }
|
|
603
|
-
housekeep_parser = OptionParser.new do |
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
end
|
|
618
|
-
housekeep_parser.parse!(
|
|
619
|
-
|
|
620
|
-
if options[ :all ] && !
|
|
621
|
-
|
|
604
|
+
housekeep_parser = OptionParser.new do |parser|
|
|
605
|
+
parser.banner = "Usage: carson housekeep [REPO] [--all] [--json]"
|
|
606
|
+
parser.separator ""
|
|
607
|
+
parser.separator "Run housekeeping: sync main, reap dead worktrees, and prune stale branches."
|
|
608
|
+
parser.separator "Defaults to the current repository."
|
|
609
|
+
parser.separator ""
|
|
610
|
+
parser.separator "Options:"
|
|
611
|
+
parser.on( "--all", "Housekeep all governed repositories" ) { options[ :all ] = true }
|
|
612
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
613
|
+
parser.separator ""
|
|
614
|
+
parser.separator "Examples:"
|
|
615
|
+
parser.separator " carson housekeep Housekeep the current repository"
|
|
616
|
+
parser.separator " carson housekeep nexus Housekeep a named governed repo"
|
|
617
|
+
parser.separator " carson housekeep --all Housekeep all governed repos"
|
|
618
|
+
end
|
|
619
|
+
housekeep_parser.parse!( arguments )
|
|
620
|
+
|
|
621
|
+
if options[ :all ] && !arguments.empty?
|
|
622
|
+
error.puts "#{BADGE} --all and repo target are mutually exclusive. Use: carson housekeep --all OR carson housekeep [repo]"
|
|
622
623
|
return { command: :invalid }
|
|
623
624
|
end
|
|
624
625
|
|
|
625
626
|
return { command: "housekeep:all", json: options[ :json ] } if options[ :all ]
|
|
626
627
|
|
|
627
|
-
if
|
|
628
|
-
|
|
628
|
+
if arguments.length > 1
|
|
629
|
+
error.puts "#{BADGE} Too many arguments for housekeep. Use: carson housekeep [repo]"
|
|
629
630
|
return { command: :invalid }
|
|
630
631
|
end
|
|
631
632
|
|
|
632
|
-
target =
|
|
633
|
+
target = arguments.shift
|
|
633
634
|
return { command: "housekeep:target", target: target, json: options[ :json ] } if target
|
|
634
635
|
|
|
635
636
|
{ command: "housekeep", json: options[ :json ] }
|
|
636
|
-
rescue OptionParser::ParseError =>
|
|
637
|
-
|
|
637
|
+
rescue OptionParser::ParseError => exception
|
|
638
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
638
639
|
{ command: :invalid }
|
|
639
640
|
end
|
|
640
641
|
|
|
641
642
|
# --- govern ---
|
|
642
643
|
|
|
643
|
-
def self.parse_govern_subcommand(
|
|
644
|
+
def self.parse_govern_subcommand( arguments:, error: )
|
|
644
645
|
options = {
|
|
645
646
|
dry_run: false,
|
|
646
647
|
json: false,
|
|
647
648
|
loop_seconds: nil
|
|
648
649
|
}
|
|
649
|
-
govern_parser = OptionParser.new do |
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
options[ :loop_seconds ] =
|
|
650
|
+
govern_parser = OptionParser.new do |parser|
|
|
651
|
+
parser.banner = "Usage: carson govern [--dry-run] [--json] [--loop SECONDS]"
|
|
652
|
+
parser.separator ""
|
|
653
|
+
parser.separator "Portfolio-level PR triage loop."
|
|
654
|
+
parser.separator "Scans governed repositories, classifies open PRs, and takes action"
|
|
655
|
+
parser.separator "(merge, request review, or report). Runs once by default."
|
|
656
|
+
parser.separator ""
|
|
657
|
+
parser.separator "Options:"
|
|
658
|
+
parser.on( "--dry-run", "Run all checks but do not merge or dispatch" ) { options[ :dry_run ] = true }
|
|
659
|
+
parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
|
|
660
|
+
parser.on( "--loop SECONDS", Integer, "Run continuously, sleeping SECONDS between cycles" ) do |seconds|
|
|
661
|
+
error.puts( "#{BADGE} Error: --loop must be a positive integer" ) || ( return { command: :invalid } ) if seconds < 1
|
|
662
|
+
options[ :loop_seconds ] = seconds
|
|
662
663
|
end
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
end
|
|
669
|
-
govern_parser.parse!(
|
|
670
|
-
unless
|
|
671
|
-
|
|
672
|
-
|
|
664
|
+
parser.separator ""
|
|
665
|
+
parser.separator "Examples:"
|
|
666
|
+
parser.separator " carson govern Triage all governed repos once"
|
|
667
|
+
parser.separator " carson govern --dry-run Preview actions without applying them"
|
|
668
|
+
parser.separator " carson govern --loop 300 Run continuously every 5 minutes"
|
|
669
|
+
end
|
|
670
|
+
govern_parser.parse!( arguments )
|
|
671
|
+
unless arguments.empty?
|
|
672
|
+
error.puts "#{BADGE} Unexpected arguments for govern: #{arguments.join( ' ' )}"
|
|
673
|
+
error.puts govern_parser
|
|
673
674
|
return { command: :invalid }
|
|
674
675
|
end
|
|
675
676
|
{
|
|
@@ -678,9 +679,9 @@ module Carson
|
|
|
678
679
|
json: options.fetch( :json ),
|
|
679
680
|
loop_seconds: options[ :loop_seconds ]
|
|
680
681
|
}
|
|
681
|
-
rescue OptionParser::ParseError =>
|
|
682
|
-
|
|
683
|
-
|
|
682
|
+
rescue OptionParser::ParseError => exception
|
|
683
|
+
error.puts "#{BADGE} #{exception.message}"
|
|
684
|
+
error.puts govern_parser
|
|
684
685
|
{ command: :invalid }
|
|
685
686
|
end
|
|
686
687
|
|