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