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.
- checksums.yaml +4 -4
- data/RELEASE.md +11 -0
- data/VERSION +1 -1
- data/lib/carson/cli.rb +372 -63
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5474bed385b09305a9a467655447faa7dc5b908486cf69d3124cca99cd7cc2f2
|
|
4
|
+
data.tar.gz: 4efa36d52f6642967abc25b35ae5a9bcfcfb97072064478873c99693c90a54cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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,
|
|
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
|
|
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:,
|
|
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,
|
|
79
|
-
when "onboard"
|
|
80
|
-
|
|
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,
|
|
105
|
+
parse_refresh_command( argv: argv, err: err )
|
|
83
106
|
when "template"
|
|
84
|
-
parse_template_subcommand( argv: argv,
|
|
107
|
+
parse_template_subcommand( argv: argv, err: err )
|
|
85
108
|
when "prune"
|
|
86
|
-
parse_prune_command( argv: argv,
|
|
109
|
+
parse_prune_command( argv: argv, err: err )
|
|
87
110
|
when "worktree"
|
|
88
|
-
parse_worktree_subcommand( argv: argv,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
|
137
|
-
err.puts
|
|
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:
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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
|
|
246
|
+
err.puts refresh_parser
|
|
155
247
|
return { command: :invalid }
|
|
156
248
|
end
|
|
157
249
|
|
|
158
|
-
return { command: "refresh:all" } if
|
|
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
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
217
|
-
err.puts
|
|
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: "
|
|
375
|
+
{ command: "review:#{action}" }
|
|
376
|
+
rescue OptionParser::ParseError => e
|
|
377
|
+
err.puts "#{BADGE} #{e.message}"
|
|
378
|
+
{ command: :invalid }
|
|
221
379
|
end
|
|
222
380
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
317
|
-
|
|
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
|
|
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:
|
|
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:
|
|
623
|
+
return { command: "housekeep:target", target: target, json: options[ :json ] } if target
|
|
333
624
|
|
|
334
|
-
{ command: "housekeep", json:
|
|
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
|