carson 3.14.0 → 3.15.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6187204d01b1fc7f62657f451451a0124e039b265d83379ce8c23e9ba62a346
4
- data.tar.gz: d5b11033558afc8847386858bc62d9ba19816636dece60dfe4a5f1708528a50f
3
+ metadata.gz: 83272d790537e52d22639bbf4831f772496b9ffa5938b23da6d2865ae73d8e38
4
+ data.tar.gz: ff4d8db79fc54bcd12d74a4c6c74332861ea58dc3fef78f93b3188237d27c772
5
5
  SHA512:
6
- metadata.gz: 005575df223f30f900794447f012d29bfa3c6a939433870da80dd54cdbcb43dd9099413357a049232ffdcd7d7c502996e3d4279a40f310fde576fe991c8659a0
7
- data.tar.gz: 9c4c5023488bce12162fbcb0c4746db515c72e2b28618ca35682a3076ec2b3c1c67513af7712cb4238dc76a2fd389f9abf75385baf6d63ea10970be960d949e9
6
+ metadata.gz: 655f2cb1f8caa036e96dc17e4e360ced9256840ee5f2050d33e9f72f4187a2f0d8fefbe27d82ff2da8e3bd21f5c8bb460886bf31af922818fa74efeb3fe9d59f
7
+ data.tar.gz: 44490df6057142b3f58745ac24041e501207599684d9a10816ee505ed192c681661fd4e23003d637a6ccf69f4a86efbd6d754ada2d6079ee99db89b431133495
data/RELEASE.md CHANGED
@@ -5,15 +5,31 @@ 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.15.0
9
+
10
+ ### What changed
11
+
12
+ - **`carson housekeep` command** — sync + prune for repositories. Carson knocks each gate humbly:
13
+ - `carson housekeep` — serve the repo you are standing in.
14
+ - `carson housekeep <repo>` — serve a named governed repo (resolved by basename, case-insensitive).
15
+ - `carson housekeep --all` — knock each governed repo's gate in turn.
16
+ - Supports `--json` for machine-readable output in all three modes.
17
+
18
+ ### UX improvement
19
+
20
+ - Fixed double-badge display when housekeep summarises prune results.
21
+ - JSON mode suppresses human-readable lines cleanly.
22
+
8
23
  ## 3.14.0
9
24
 
10
25
  ### What changed
11
26
 
12
27
  - **`carson repos` command** — lists all governed repositories from Carson's global config. Shows which repos Carson is serving at a glance. Supports `--json` for machine-readable output. Portfolio-level command — works from any directory.
28
+ - **Auto-register on onboard** — `carson onboard` now automatically registers the repository for portfolio governance. No more TTY-only Y/n prompt — onboard means govern.
13
29
 
14
30
  ### UX improvement
15
31
 
16
- - Bots and humans can now verify which repos Carson oversees with a single command instead of reading raw config JSON.
32
+ - Onboarding is now one step: `carson onboard` sets up hooks, templates, and portfolio registration in a single pass. Works identically in TTY and non-TTY (agent) sessions.
17
33
 
18
34
  ## 3.13.2
19
35
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.14.0
1
+ 3.15.0
data/lib/carson/cli.rb CHANGED
@@ -12,7 +12,7 @@ module Carson
12
12
  return Runtime::EXIT_OK
13
13
  end
14
14
 
15
- if %w[repos refresh:all prune:all].include?( command )
15
+ if %w[repos refresh:all prune:all housekeep:all housekeep:target].include?( command )
16
16
  verbose = parsed.fetch( :verbose, false )
17
17
  runtime = Runtime.new( repo_root: repo_root, tool_root: tool_root, out: out, err: err, verbose: verbose )
18
18
  return dispatch( parsed: parsed, runtime: runtime )
@@ -53,7 +53,7 @@ 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>|repos [--json]|onboard|refresh [--all]|offboard|template check|apply|review gate|sweep|govern [--dry-run] [--json] [--loop SECONDS]|version]"
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]"
57
57
  end
58
58
  end
59
59
 
@@ -88,6 +88,8 @@ module Carson
88
88
  parse_worktree_subcommand( argv: argv, parser: parser, err: err )
89
89
  when "repos"
90
90
  parse_repos_command( argv: argv, err: err )
91
+ when "housekeep"
92
+ parse_housekeep_command( argv: argv, err: err )
91
93
  when "review"
92
94
  parse_named_subcommand( command: command, usage: "gate|sweep", argv: argv, parser: parser, err: err )
93
95
  when "audit"
@@ -310,6 +312,28 @@ module Carson
310
312
  { command: "repos", json: json_flag }
311
313
  end
312
314
 
315
+ def self.parse_housekeep_command( argv:, err: )
316
+ all_flag = argv.delete( "--all" ) ? true : false
317
+ json_flag = argv.delete( "--json" ) ? true : false
318
+
319
+ if all_flag && !argv.empty?
320
+ err.puts "#{BADGE} --all and repo target are mutually exclusive. Use: carson housekeep --all OR carson housekeep [repo]"
321
+ return { command: :invalid }
322
+ end
323
+
324
+ return { command: "housekeep:all", json: json_flag } if all_flag
325
+
326
+ if argv.length > 1
327
+ err.puts "#{BADGE} Too many arguments for housekeep. Use: carson housekeep [repo]"
328
+ return { command: :invalid }
329
+ end
330
+
331
+ target = argv.shift
332
+ return { command: "housekeep:target", target: target, json: json_flag } if target
333
+
334
+ { command: "housekeep", json: json_flag }
335
+ end
336
+
313
337
  def self.parse_govern_subcommand( argv:, err: )
314
338
  options = {
315
339
  dry_run: false,
@@ -389,6 +413,12 @@ module Carson
389
413
  runtime.review_sweep!
390
414
  when "repos"
391
415
  runtime.repos!( json_output: parsed.fetch( :json, false ) )
416
+ when "housekeep"
417
+ runtime.housekeep!( json_output: parsed.fetch( :json, false ) )
418
+ when "housekeep:target"
419
+ runtime.housekeep_target!( target: parsed.fetch( :target ), json_output: parsed.fetch( :json, false ) )
420
+ when "housekeep:all"
421
+ runtime.housekeep_all!( json_output: parsed.fetch( :json, false ) )
392
422
  when "govern"
393
423
  runtime.govern!(
394
424
  dry_run: parsed.fetch( :dry_run, false ),
@@ -0,0 +1,120 @@
1
+ # Housekeeping — sync + prune for a repository.
2
+ # carson housekeep <repo> — serve one repo by name or path.
3
+ # carson housekeep — serve the repo you are standing in.
4
+ # carson housekeep --all — serve all governed repos.
5
+ require "json"
6
+ require "stringio"
7
+
8
+ module Carson
9
+ class Runtime
10
+ module Housekeep
11
+ # Serves the current repo: sync + prune.
12
+ def housekeep!( json_output: false )
13
+ housekeep_one( repo_path: repo_root, json_output: json_output )
14
+ end
15
+
16
+ # Resolves a target name to a governed repo, then serves it.
17
+ def housekeep_target!( target:, json_output: false )
18
+ repo_path = resolve_governed_repo( target: target )
19
+ unless repo_path
20
+ result = { command: "housekeep", status: "error", error: "Not a governed repository: #{target}", recovery: "Run carson repos to see governed repositories." }
21
+ return housekeep_finish( result: result, exit_code: EXIT_ERROR, json_output: json_output )
22
+ end
23
+
24
+ housekeep_one( repo_path: repo_path, json_output: json_output )
25
+ end
26
+
27
+ # Knocks each governed repo's gate in turn.
28
+ def housekeep_all!( json_output: false )
29
+ repos = config.govern_repos
30
+ if repos.empty?
31
+ result = { command: "housekeep", status: "error", error: "No governed repositories configured.", recovery: "Run carson onboard in each repo to register." }
32
+ return housekeep_finish( result: result, exit_code: EXIT_ERROR, json_output: json_output )
33
+ end
34
+
35
+ results = []
36
+ repos.each { |repo_path| results << housekeep_one_entry( repo_path: repo_path, silent: json_output ) }
37
+
38
+ succeeded = results.count { |r| r[ :status ] == "ok" }
39
+ failed = results.count { |r| r[ :status ] != "ok" }
40
+ result = { command: "housekeep", status: failed.zero? ? "ok" : "partial", repos: results, succeeded: succeeded, failed: failed }
41
+ housekeep_finish( result: result, exit_code: failed.zero? ? EXIT_OK : EXIT_ERROR, json_output: json_output, results: results, succeeded: succeeded, failed: failed )
42
+ end
43
+
44
+ private
45
+
46
+ # Runs sync + prune on one repo and returns the exit code directly.
47
+ def housekeep_one( repo_path:, json_output: false )
48
+ entry = housekeep_one_entry( repo_path: repo_path, silent: json_output )
49
+ ok = entry[ :status ] == "ok"
50
+ result = { command: "housekeep", status: ok ? "ok" : "error", repos: [ entry ], succeeded: ok ? 1 : 0, failed: ok ? 0 : 1 }
51
+ housekeep_finish( result: result, exit_code: ok ? EXIT_OK : EXIT_ERROR, json_output: json_output, results: [ entry ], succeeded: result[ :succeeded ], failed: result[ :failed ] )
52
+ end
53
+
54
+ # Runs sync + prune on a single repository. Returns a result hash.
55
+ def housekeep_one_entry( repo_path:, silent: false )
56
+ repo_name = File.basename( repo_path )
57
+ unless Dir.exist?( repo_path )
58
+ puts_line "#{repo_name}: SKIP (path not found)" unless silent
59
+ return { name: repo_name, path: repo_path, status: "error", error: "path not found" }
60
+ end
61
+
62
+ buf = verbose? ? out : StringIO.new
63
+ err_buf = verbose? ? err : StringIO.new
64
+ rt = Runtime.new( repo_root: repo_path, tool_root: tool_root, out: buf, err: err_buf, verbose: verbose? )
65
+
66
+ sync_status = rt.sync!
67
+ prune_status = rt.prune! if sync_status == EXIT_OK
68
+
69
+ ok = sync_status == EXIT_OK && prune_status == EXIT_OK
70
+ unless verbose? || silent
71
+ summary = strip_badge( buf.string.lines.last.to_s.strip )
72
+ puts_line "#{repo_name}: #{summary.empty? ? 'OK' : summary}"
73
+ end
74
+
75
+ { name: repo_name, path: repo_path, status: ok ? "ok" : "error" }
76
+ rescue StandardError => e
77
+ puts_line "#{repo_name}: FAIL (#{e.message})" unless silent
78
+ { name: repo_name, path: repo_path, status: "error", error: e.message }
79
+ end
80
+
81
+ # Strips the Carson badge prefix from a message to avoid double-badging.
82
+ def strip_badge( text )
83
+ text.sub( /\A#{Regexp.escape( BADGE )}\s*/, "" )
84
+ end
85
+
86
+ # Resolves a user-supplied target to a governed repository path.
87
+ # Accepts: exact path, expandable path, or basename match (case-insensitive).
88
+ def resolve_governed_repo( target: )
89
+ repos = config.govern_repos
90
+ expanded = File.expand_path( target )
91
+ return expanded if repos.include?( expanded )
92
+
93
+ downcased = File.basename( target ).downcase
94
+ repos.find { |r| File.basename( r ).downcase == downcased }
95
+ end
96
+
97
+ # Unified output — JSON or human-readable.
98
+ def housekeep_finish( result:, exit_code:, json_output:, results: nil, succeeded: nil, failed: nil )
99
+ result[ :exit_code ] = exit_code
100
+
101
+ if json_output
102
+ out.puts JSON.pretty_generate( result )
103
+ else
104
+ if results && ( succeeded || failed )
105
+ total = ( succeeded || 0 ) + ( failed || 0 )
106
+ puts_line ""
107
+ puts_line "Housekeep complete: #{succeeded} cleaned, #{failed} failed (#{total} repo#{plural_suffix( count: total )})."
108
+ elsif result[ :error ]
109
+ puts_line result[ :error ]
110
+ puts_line " #{result[ :recovery ]}" if result[ :recovery ]
111
+ end
112
+ end
113
+
114
+ exit_code
115
+ end
116
+ end
117
+
118
+ include Housekeep
119
+ end
120
+ end
@@ -217,6 +217,7 @@ end
217
217
 
218
218
  require_relative "runtime/local"
219
219
  require_relative "runtime/audit"
220
+ require_relative "runtime/housekeep"
220
221
  require_relative "runtime/repos"
221
222
  require_relative "runtime/review"
222
223
  require_relative "runtime/govern"
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.14.0
4
+ version: 3.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang
@@ -54,6 +54,7 @@ files:
54
54
  - lib/carson/runtime/audit.rb
55
55
  - lib/carson/runtime/deliver.rb
56
56
  - lib/carson/runtime/govern.rb
57
+ - lib/carson/runtime/housekeep.rb
57
58
  - lib/carson/runtime/local.rb
58
59
  - lib/carson/runtime/local/hooks.rb
59
60
  - lib/carson/runtime/local/onboard.rb