carson 3.3.0 → 3.5.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 +33 -0
- data/VERSION +1 -1
- data/lib/carson/cli.rb +25 -3
- data/lib/carson/runtime/audit.rb +47 -10
- data/lib/carson/runtime/local/sync.rb +56 -12
- 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: e8fc2b54698568aaec7d7f4ec39ecd32f56f44a395555a34099b24ec52a1d608
|
|
4
|
+
data.tar.gz: e3b0a5a14c733d6b2e9f5c08475d89a53f0a3d1faf8a2fb85ed72bcb76f662ef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9d3733f98a23dffdc69c1ebde71e94595c22e662f5ead86b97988fa3f5cf0e8daf56806deb82ceb1d034e0913640d9a8b7cafc26992f94e43dd96a3e304d6812
|
|
7
|
+
data.tar.gz: eb44eca84a943a461668b478e2bed295b3a73ee7e916cc5d85a837c52f5be09326b369e07a99812d4ffdc883914511d18c7f0489e151f3d4d66afe8ddaf97402
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,39 @@ 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.5.0 — Sync JSON + Recovery
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **`carson sync --json`** — machine-readable JSON output for the sync command. The JSON envelope includes `command`, `status`, `ahead`, `behind`, `main_branch`, `remote`, `exit_code`, and on failure: `error` and `recovery`.
|
|
13
|
+
- **Recovery-aware sync errors** — dirty working tree now includes a recovery command in both human (`Recovery: ...`) and JSON output.
|
|
14
|
+
|
|
15
|
+
### UX
|
|
16
|
+
|
|
17
|
+
- JSON output suppresses git command output (fetch, pull) to keep the JSON envelope clean.
|
|
18
|
+
- Human output remains unchanged when `--json` is not passed.
|
|
19
|
+
|
|
20
|
+
### Migration
|
|
21
|
+
|
|
22
|
+
- No breaking changes. `carson sync` without `--json` behaves identically to 3.4.0.
|
|
23
|
+
|
|
24
|
+
## 3.4.0 — Audit JSON
|
|
25
|
+
|
|
26
|
+
### What changed
|
|
27
|
+
|
|
28
|
+
- **`carson audit --json`** — machine-readable JSON output for the audit command. The JSON envelope includes `command`, `status`, `branch`, `hooks`, `main_sync`, `pr`, `checks`, `baseline`, `problems`, and `exit_code`. Agents can parse audit results programmatically instead of regex-matching human-readable text.
|
|
29
|
+
|
|
30
|
+
### UX
|
|
31
|
+
|
|
32
|
+
- JSON output is only produced when `--json` is passed; default human-readable output is unchanged.
|
|
33
|
+
- The `problems` array contains the same concise problem strings shown in non-verbose human output, making it easy for agents to surface actionable issues.
|
|
34
|
+
- `hooks.status` is `"ok"` or `"mismatch"`, `main_sync.status` is `"ok"`, `"ahead"`, `"behind"`, or `"unknown"`.
|
|
35
|
+
|
|
36
|
+
### Migration
|
|
37
|
+
|
|
38
|
+
- No breaking changes. `carson audit` without `--json` behaves identically to 3.3.0.
|
|
39
|
+
- The `audit!` method now accepts `json_output:` keyword argument (default `false`).
|
|
40
|
+
|
|
8
41
|
## 3.3.0 — Deliver Refinements
|
|
9
42
|
|
|
10
43
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.5.0
|
data/lib/carson/cli.rb
CHANGED
|
@@ -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|sync|deliver [--merge] [--json] [--title T] [--body-file F]|prune [--all]|worktree create|done|remove <name>|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]|worktree create|done|remove <name>|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,10 @@ module Carson
|
|
|
88
88
|
parse_worktree_subcommand( argv: argv, parser: parser, err: err )
|
|
89
89
|
when "review"
|
|
90
90
|
parse_named_subcommand( command: command, usage: "gate|sweep", argv: argv, parser: parser, err: err )
|
|
91
|
+
when "audit"
|
|
92
|
+
parse_audit_command( argv: argv, err: err )
|
|
93
|
+
when "sync"
|
|
94
|
+
parse_sync_command( argv: argv, err: err )
|
|
91
95
|
when "status"
|
|
92
96
|
parse_status_command( argv: argv, err: err )
|
|
93
97
|
when "deliver"
|
|
@@ -242,6 +246,24 @@ module Carson
|
|
|
242
246
|
{ command: :invalid }
|
|
243
247
|
end
|
|
244
248
|
|
|
249
|
+
def self.parse_audit_command( argv:, err: )
|
|
250
|
+
json_flag = argv.delete( "--json" ) ? true : false
|
|
251
|
+
unless argv.empty?
|
|
252
|
+
err.puts "#{BADGE} Unexpected arguments for audit: #{argv.join( ' ' )}"
|
|
253
|
+
return { command: :invalid }
|
|
254
|
+
end
|
|
255
|
+
{ command: "audit", json: json_flag }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def self.parse_sync_command( argv:, err: )
|
|
259
|
+
json_flag = argv.delete( "--json" ) ? true : false
|
|
260
|
+
unless argv.empty?
|
|
261
|
+
err.puts "#{BADGE} Unexpected arguments for sync: #{argv.join( ' ' )}"
|
|
262
|
+
return { command: :invalid }
|
|
263
|
+
end
|
|
264
|
+
{ command: "sync", json: json_flag }
|
|
265
|
+
end
|
|
266
|
+
|
|
245
267
|
def self.parse_status_command( argv:, err: )
|
|
246
268
|
json_flag = argv.delete( "--json" ) ? true : false
|
|
247
269
|
unless argv.empty?
|
|
@@ -321,9 +343,9 @@ module Carson
|
|
|
321
343
|
when "setup"
|
|
322
344
|
runtime.setup!( cli_choices: parsed.fetch( :cli_choices, {} ) )
|
|
323
345
|
when "audit"
|
|
324
|
-
runtime.audit!
|
|
346
|
+
runtime.audit!( json_output: parsed.fetch( :json, false ) )
|
|
325
347
|
when "sync"
|
|
326
|
-
runtime.sync!
|
|
348
|
+
runtime.sync!( json_output: parsed.fetch( :json, false ) )
|
|
327
349
|
when "prune"
|
|
328
350
|
runtime.prune!
|
|
329
351
|
when "prune:all"
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
+
# Pre-commit audit — checks hooks, main sync, PR checks, and CI baseline.
|
|
2
|
+
# Exits with EXIT_BLOCK when policy violations are found.
|
|
3
|
+
# Supports --json for machine-readable structured output.
|
|
1
4
|
require "cgi"
|
|
2
5
|
|
|
3
6
|
module Carson
|
|
4
7
|
class Runtime
|
|
5
8
|
module Audit
|
|
6
|
-
def audit!
|
|
9
|
+
def audit!( json_output: false )
|
|
7
10
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
8
11
|
return fingerprint_status unless fingerprint_status.nil?
|
|
9
12
|
unless head_exists?
|
|
10
|
-
|
|
13
|
+
if json_output
|
|
14
|
+
out.puts JSON.pretty_generate( { command: "audit", status: "skipped", reason: "no commits yet", exit_code: EXIT_OK } )
|
|
15
|
+
else
|
|
16
|
+
puts_line "No commits yet — audit skipped for initial commit."
|
|
17
|
+
end
|
|
11
18
|
return EXIT_OK
|
|
12
19
|
end
|
|
13
20
|
audit_state = "ok"
|
|
@@ -22,6 +29,7 @@ module Carson
|
|
|
22
29
|
puts_verbose ""
|
|
23
30
|
puts_verbose "[Hooks]"
|
|
24
31
|
hooks_ok = hooks_health_report
|
|
32
|
+
hooks_status = hooks_ok ? "ok" : "mismatch"
|
|
25
33
|
unless hooks_ok
|
|
26
34
|
audit_state = "block"
|
|
27
35
|
audit_concise_problems << "Hooks: mismatch — run carson refresh."
|
|
@@ -29,23 +37,27 @@ module Carson
|
|
|
29
37
|
puts_verbose ""
|
|
30
38
|
puts_verbose "[Main Sync Status]"
|
|
31
39
|
ahead_count, behind_count, main_error = main_sync_counts
|
|
40
|
+
main_sync = { ahead: 0, behind: 0, status: "ok" }
|
|
32
41
|
if main_error
|
|
33
42
|
puts_verbose "main_vs_remote_main: unknown"
|
|
34
43
|
puts_verbose "WARN: unable to calculate main sync status (#{main_error})."
|
|
35
44
|
audit_state = "attention" if audit_state == "ok"
|
|
36
45
|
audit_concise_problems << "Main sync: unable to determine — check remote connectivity."
|
|
46
|
+
main_sync = { ahead: 0, behind: 0, status: "unknown", error: main_error }
|
|
37
47
|
elsif ahead_count.positive?
|
|
38
48
|
puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
|
|
39
49
|
puts_verbose "main_vs_remote_main_behind: #{behind_count}"
|
|
40
50
|
puts_verbose "ACTION: local #{config.main_branch} is ahead of #{config.git_remote}/#{config.main_branch} by #{ahead_count} commit#{plural_suffix( count: ahead_count )}; reset local drift before commit/push workflows."
|
|
41
51
|
audit_state = "block"
|
|
42
52
|
audit_concise_problems << "Main sync (#{config.git_remote}): ahead by #{ahead_count} — git fetch #{config.git_remote}, or carson setup to switch remote."
|
|
53
|
+
main_sync = { ahead: ahead_count, behind: behind_count, status: "ahead" }
|
|
43
54
|
elsif behind_count.positive?
|
|
44
55
|
puts_verbose "main_vs_remote_main_ahead: #{ahead_count}"
|
|
45
56
|
puts_verbose "main_vs_remote_main_behind: #{behind_count}"
|
|
46
57
|
puts_verbose "ACTION: local #{config.main_branch} is behind #{config.git_remote}/#{config.main_branch} by #{behind_count} commit#{plural_suffix( count: behind_count )}; run carson sync."
|
|
47
58
|
audit_state = "attention" if audit_state == "ok"
|
|
48
59
|
audit_concise_problems << "Main sync (#{config.git_remote}): behind by #{behind_count} — run carson sync."
|
|
60
|
+
main_sync = { ahead: ahead_count, behind: behind_count, status: "behind" }
|
|
49
61
|
else
|
|
50
62
|
puts_verbose "main_vs_remote_main_ahead: 0"
|
|
51
63
|
puts_verbose "main_vs_remote_main_behind: 0"
|
|
@@ -109,15 +121,40 @@ module Carson
|
|
|
109
121
|
audit_status: audit_state
|
|
110
122
|
)
|
|
111
123
|
)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
exit_code = audit_state == "block" ? EXIT_BLOCK : EXIT_OK
|
|
125
|
+
|
|
126
|
+
if json_output
|
|
127
|
+
result = {
|
|
128
|
+
command: "audit",
|
|
129
|
+
status: audit_state,
|
|
130
|
+
branch: current_branch,
|
|
131
|
+
hooks: { status: hooks_status },
|
|
132
|
+
main_sync: main_sync,
|
|
133
|
+
pr: monitor_report[ :pr ],
|
|
134
|
+
checks: monitor_report.fetch( :checks ),
|
|
135
|
+
baseline: {
|
|
136
|
+
status: default_branch_baseline.fetch( :status ),
|
|
137
|
+
repository: default_branch_baseline[ :repository ],
|
|
138
|
+
failing_count: default_branch_baseline.fetch( :failing_count ),
|
|
139
|
+
pending_count: default_branch_baseline.fetch( :pending_count ),
|
|
140
|
+
advisory_failing_count: default_branch_baseline.fetch( :advisory_failing_count ),
|
|
141
|
+
advisory_pending_count: default_branch_baseline.fetch( :advisory_pending_count )
|
|
142
|
+
},
|
|
143
|
+
problems: audit_concise_problems,
|
|
144
|
+
exit_code: exit_code
|
|
145
|
+
}
|
|
146
|
+
out.puts JSON.pretty_generate( result )
|
|
147
|
+
else
|
|
148
|
+
puts_verbose ""
|
|
149
|
+
puts_verbose "[Audit Result]"
|
|
150
|
+
puts_verbose "status: #{audit_state}"
|
|
151
|
+
puts_verbose( audit_state == "block" ? "ACTION: local policy block must be resolved before commit/push." : "ACTION: no local hard block detected." )
|
|
152
|
+
unless verbose?
|
|
153
|
+
audit_concise_problems.each { |problem| puts_line problem }
|
|
154
|
+
puts_line "Audit: #{audit_state}"
|
|
155
|
+
end
|
|
119
156
|
end
|
|
120
|
-
|
|
157
|
+
exit_code
|
|
121
158
|
end
|
|
122
159
|
|
|
123
160
|
private
|
|
@@ -1,39 +1,83 @@
|
|
|
1
|
+
# Syncs local main branch with remote main.
|
|
2
|
+
# Supports --json for machine-readable structured output.
|
|
1
3
|
module Carson
|
|
2
4
|
class Runtime
|
|
3
5
|
module Local
|
|
4
|
-
def sync!
|
|
6
|
+
def sync!( json_output: false )
|
|
5
7
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
6
8
|
return fingerprint_status unless fingerprint_status.nil?
|
|
7
9
|
|
|
8
10
|
unless working_tree_clean?
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
return sync_finish(
|
|
12
|
+
result: { command: "sync", status: "block", error: "working tree is dirty", recovery: "git add -A && git commit, then carson sync" },
|
|
13
|
+
exit_code: EXIT_BLOCK, json_output: json_output
|
|
14
|
+
)
|
|
11
15
|
end
|
|
12
16
|
start_branch = current_branch
|
|
13
17
|
switched = false
|
|
14
|
-
|
|
18
|
+
sync_git!( "fetch", config.git_remote, "--prune", json_output: json_output )
|
|
15
19
|
if start_branch != config.main_branch
|
|
16
|
-
|
|
20
|
+
sync_git!( "switch", config.main_branch, json_output: json_output )
|
|
17
21
|
switched = true
|
|
18
22
|
end
|
|
19
|
-
|
|
23
|
+
sync_git!( "pull", "--ff-only", config.git_remote, config.main_branch, json_output: json_output )
|
|
20
24
|
ahead_count, behind_count, error_text = main_sync_counts
|
|
21
25
|
if error_text
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
return sync_finish(
|
|
27
|
+
result: { command: "sync", status: "block", error: "unable to verify main sync state (#{error_text})" },
|
|
28
|
+
exit_code: EXIT_BLOCK, json_output: json_output
|
|
29
|
+
)
|
|
24
30
|
end
|
|
25
31
|
if ahead_count.zero? && behind_count.zero?
|
|
26
|
-
|
|
27
|
-
|
|
32
|
+
return sync_finish(
|
|
33
|
+
result: { command: "sync", status: "ok", ahead: 0, behind: 0, main_branch: config.main_branch, remote: config.git_remote },
|
|
34
|
+
exit_code: EXIT_OK, json_output: json_output
|
|
35
|
+
)
|
|
28
36
|
end
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
sync_finish(
|
|
38
|
+
result: { command: "sync", status: "block", ahead: ahead_count, behind: behind_count, main_branch: config.main_branch, remote: config.git_remote, error: "local #{config.main_branch} still diverges" },
|
|
39
|
+
exit_code: EXIT_BLOCK, json_output: json_output
|
|
40
|
+
)
|
|
31
41
|
ensure
|
|
32
42
|
git_system!( "switch", start_branch ) if switched && branch_exists?( branch_name: start_branch )
|
|
33
43
|
end
|
|
34
44
|
|
|
35
45
|
private
|
|
36
46
|
|
|
47
|
+
# Runs a git command, suppressing stdout/stderr in JSON mode to keep output clean.
|
|
48
|
+
def sync_git!( *args, json_output: false )
|
|
49
|
+
if json_output
|
|
50
|
+
_, stderr_text, success, = git_run( *args )
|
|
51
|
+
raise "git #{args.join( ' ' )} failed: #{stderr_text.to_s.strip}" unless success
|
|
52
|
+
else
|
|
53
|
+
git_system!( *args )
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Unified output for sync results — JSON or human-readable.
|
|
58
|
+
def sync_finish( result:, exit_code:, json_output: )
|
|
59
|
+
result[ :exit_code ] = exit_code
|
|
60
|
+
|
|
61
|
+
if json_output
|
|
62
|
+
out.puts JSON.pretty_generate( result )
|
|
63
|
+
else
|
|
64
|
+
print_sync_human( result: result )
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
exit_code
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Human-readable output for sync results.
|
|
71
|
+
def print_sync_human( result: )
|
|
72
|
+
if result[ :error ]
|
|
73
|
+
puts_line "BLOCK: #{result[ :error ]}."
|
|
74
|
+
puts_line " Recovery: #{result[ :recovery ]}" if result[ :recovery ]
|
|
75
|
+
return
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
puts_line "OK: local #{result[ :main_branch ]} is now in sync with #{result[ :remote ]}/#{result[ :main_branch ]}."
|
|
79
|
+
end
|
|
80
|
+
|
|
37
81
|
# Returns ahead/behind counts for local main versus configured remote main.
|
|
38
82
|
def main_sync_counts
|
|
39
83
|
target = "#{config.main_branch}...#{config.git_remote}/#{config.main_branch}"
|