Module: Carson::Runtime::Local
- Included in:
- Carson::Runtime
- Defined in:
- lib/carson/runtime/local/sync.rb,
lib/carson/runtime/local/hooks.rb,
lib/carson/runtime/local/prune.rb,
lib/carson/runtime/local/onboard.rb,
lib/carson/runtime/local/template.rb,
lib/carson/runtime/local/worktree.rb
Constant Summary collapse
- TEMPLATE_SYNC_BRANCH =
"carson/template-sync".freeze
- SUPERSEDED =
[ ".github/carson-instructions.md", ".github/biome.json", ".github/erb-lint.yml", ".github/rubocop.yml", ".github/ruff.toml", ".github/workflows/carson-lint.yml", ".github/.mega-linter.yml", ".github/carson.md", ".github/copilot-instructions.md", ".github/CLAUDE.md", ".github/AGENTS.md", ".github/pull_request_template.md" ].freeze
Instance Method Summary collapse
-
#cwd_worktree_branch ⇒ Object
Returns the branch checked out in the worktree that contains the process CWD, or nil if CWD is not inside any worktree.
-
#main_worktree_root ⇒ Object
Returns the main (non-worktree) repository root.
-
#offboard! ⇒ Object
Removes Carson-managed repository integration so a host repository can retire Carson cleanly.
-
#onboard! ⇒ Object
One-command onboarding for new repositories: detect remote, install hooks, apply templates, and run initial audit.
- #prune!(json_output: false) ⇒ Object
- #prune_all! ⇒ Object
-
#prune_plan(dry_run: true) ⇒ Object
Returns a plan hash describing what prune! would do, without executing any mutations.
-
#realpath_safe(path) ⇒ Object
Resolves a path to its canonical form, tolerating non-existent paths.
-
#refresh! ⇒ Object
Re-applies hooks, templates, and audit after upgrading Carson.
-
#refresh_all! ⇒ Object
Re-applies hooks, templates, and audit across all governed repositories.
-
#sweep_stale_worktrees! ⇒ Object
Removes agent-owned worktrees whose branch content is already on main.
- #sync!(json_output: false) ⇒ Object
-
#sync_all! ⇒ Object
Syncs main branch across all governed repositories.
-
#template_apply!(push_prep: false) ⇒ Object
Applies managed template files as full-file writes from Carson sources.
-
#template_check! ⇒ Object
Read-only template drift check; returns block when managed files are output of sync.
-
#template_check_all! ⇒ Object
Read-only template drift check across all governed repositories.
-
#worktree_create!(name:, json_output: false) ⇒ Object
Creates a new worktree under .claude/worktrees/<name>.
-
#worktree_list ⇒ Object
Returns all registered worktrees as Carson::Worktree instances.
-
#worktree_remove!(worktree_path:, force: false, json_output: false) ⇒ Object
Removes a worktree: directory, git registration, and branch.
Instance Method Details
#cwd_worktree_branch ⇒ Object
Returns the branch checked out in the worktree that contains the process CWD, or nil if CWD is not inside any worktree. Used by prune to proactively protect the CWD worktree’s branch from deletion. Matches the longest (most specific) path because worktree directories live under the main repo tree (.claude/worktrees/).
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/carson/runtime/local/worktree.rb', line 38 def cwd_worktree_branch cwd = realpath_safe( Dir.pwd ) best_branch = nil best_length = -1 worktree_list.each do |worktree| normalised = File.join( worktree.path, "" ) if ( cwd == worktree.path || cwd.start_with?( normalised ) ) && worktree.path.length > best_length best_branch = worktree.branch best_length = worktree.path.length end end best_branch rescue StandardError nil end |
#main_worktree_root ⇒ Object
Returns the main (non-worktree) repository root. Uses git-common-dir to find the shared .git directory, then takes its parent. Falls back to repo_root if detection fails.
57 58 59 60 61 62 |
# File 'lib/carson/runtime/local/worktree.rb', line 57 def main_worktree_root common_dir, _, success, = git_run( "rev-parse", "--path-format=absolute", "--git-common-dir" ) return File.dirname( common_dir.strip ) if success && !common_dir.strip.empty? repo_root end |
#offboard! ⇒ Object
Removes Carson-managed repository integration so a host repository can retire Carson cleanly.
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 |
# File 'lib/carson/runtime/local/onboard.rb', line 198 def offboard! puts_verbose "" puts_verbose "[Offboard]" unless inside_git_work_tree? puts_line "#{repo_root} is not a git repository." return EXIT_ERROR end if input_stream.respond_to?( :tty? ) && input_stream.tty? puts_line "" puts_line "This will remove Carson hooks, managed .github/ files," puts_line "and deregister this repository from portfolio governance." puts_line "Continue?" unless prompt_yes_no( default: false ) puts_line "Offboard cancelled." return EXIT_OK end end hooks_status = disable_carson_hooks_path! return hooks_status unless hooks_status == EXIT_OK removed_count = 0 missing_count = 0 offboard_cleanup_targets.each do |relative| absolute = resolve_repo_path!( relative_path: relative, label: "offboard target #{relative}" ) if File.exist?( absolute ) FileUtils.rm_rf( absolute ) puts_verbose "removed_path: #{relative}" removed_count += 1 else puts_verbose "skip_missing_path: #{relative}" missing_count += 1 end end remove_empty_offboard_directories! remove_govern_repo!( repo_path: File.( repo_root ) ) puts_verbose "govern_deregistered: #{File.( repo_root )}" puts_verbose "offboard_summary: removed=#{removed_count} missing=#{missing_count}" if verbose? puts_line "OK: Carson offboard completed for #{repo_root}." else puts_line "Removed #{removed_count} file#{plural_suffix( count: removed_count )}. Offboard complete." end puts_line "" puts_line "Next: commit the removals and push to finalise offboarding." EXIT_OK end |
#onboard! ⇒ Object
One-command onboarding for new repositories: detect remote, install hooks, apply templates, and run initial audit.
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/carson/runtime/local/onboard.rb', line 10 def onboard! fingerprint_status = block_if_outsider_fingerprints! return fingerprint_status unless fingerprint_status.nil? unless inside_git_work_tree? puts_line "#{repo_root} is not a git repository." return EXIT_ERROR end repo_name = File.basename( repo_root ) puts_line "" puts_line "Onboarding #{repo_name}..." if !global_config_exists? || !git_remote_exists?( remote_name: config.git_remote ) if input_stream.respond_to?( :tty? ) && input_stream.tty? setup_status = setup! return setup_status unless setup_status == EXIT_OK else silent_setup! end end onboard_apply! end |
#prune!(json_output: false) ⇒ Object
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/carson/runtime/local/prune.rb', line 69 def prune!( json_output: false ) fingerprint_status = block_if_outsider_fingerprints! unless fingerprint_status.nil? if json_output output.puts JSON.pretty_generate( { command: "prune", status: "block", error: "Carson-owned artefacts detected in host repository", recovery: "remove Carson-owned files (.carson.yml, bin/carson, .tools/carson) then retry", exit_code: EXIT_BLOCK } ) end return fingerprint_status end prune_git!( "fetch", config.git_remote, "--prune", json_output: json_output ) # Clean stale worktree entries whose directories no longer exist. # Unblocks branch deletion for branches held by dead worktrees. git_run( "worktree", "prune" ) active_branch = current_branch cwd_branch = cwd_worktree_branch counters = { deleted: 0, skipped: 0 } branches = [] stale_branches = stale_local_branches prune_stale_branch_entries( stale_branches: stale_branches, active_branch: active_branch, cwd_branch: cwd_branch, counters: counters, branches: branches ) orphan_branches = orphan_local_branches( active_branch: active_branch, cwd_branch: cwd_branch ) prune_orphan_branch_entries( orphan_branches: orphan_branches, counters: counters, branches: branches ) absorbed_branches = absorbed_local_branches( active_branch: active_branch, cwd_branch: cwd_branch ) prune_absorbed_branch_entries( absorbed_branches: absorbed_branches, counters: counters, branches: branches ) prune_finish( result: { command: "prune", status: "ok", branches: branches, deleted: counters.fetch( :deleted ), skipped: counters.fetch( :skipped ) }, exit_code: EXIT_OK, json_output: json_output, counters: counters ) end |
#prune_all! ⇒ Object
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/carson/runtime/local/onboard.rb', line 147 def prune_all! repos = config.govern_repos if repos.empty? puts_line "No governed repositories configured." puts_line " Run carson onboard in each repo to register." return EXIT_ERROR end puts_line "" puts_line "Prune all (#{repos.length} repo#{plural_suffix( count: repos.length )})" succeeded = 0 failed = 0 repos.each do |repo_path| repo_name = File.basename( repo_path ) unless Dir.exist?( repo_path ) puts_line "#{repo_name}: not found" record_batch_skip( command: "prune", repo_path: repo_path, reason: "path not found" ) failed += 1 next end begin buffer = verbose? ? output : StringIO.new error_buffer = verbose? ? error : StringIO.new scoped_runtime = Runtime.new( repo_root: repo_path, tool_root: tool_root, output: buffer, error: error_buffer, verbose: verbose? ) status = scoped_runtime.prune! unless verbose? summary = buffer.string.lines.last.to_s.strip puts_line "#{repo_name}: #{summary.empty? ? 'OK' : summary}" end if status == EXIT_ERROR record_batch_skip( command: "prune", repo_path: repo_path, reason: "prune failed" ) failed += 1 else clear_batch_success( command: "prune", repo_path: repo_path ) succeeded += 1 end rescue StandardError => exception puts_line "#{repo_name}: could not complete (#{exception.})" record_batch_skip( command: "prune", repo_path: repo_path, reason: exception. ) failed += 1 end end puts_line "" puts_line "Prune all complete: #{succeeded} pruned, #{failed} failed." failed.zero? ? EXIT_OK : EXIT_ERROR end |
#prune_plan(dry_run: true) ⇒ Object
Returns a plan hash describing what prune! would do, without executing any mutations. Does NOT fetch — branch staleness reflects whatever the last fetch left behind. Returns: { stale: […], orphan: […], absorbed: […] } Each item: { branch:, action: :delete|:skip, reason:, type: }
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/carson/runtime/local/prune.rb', line 11 def prune_plan( dry_run: true ) # rubocop:disable Lint/UnusedMethodArgument active_branch = current_branch cwd_branch = cwd_worktree_branch stale = stale_local_branches.map do |entry| branch = entry.fetch( :branch ) upstream = entry.fetch( :upstream ) if config.protected_branches.include?( branch ) { action: :skip, branch: branch, upstream: upstream, name: branch, type: "stale", reason: "protected branch" } elsif branch == active_branch { action: :skip, branch: branch, upstream: upstream, name: branch, type: "stale", reason: "current branch" } elsif cwd_branch && branch == cwd_branch { action: :skip, branch: branch, upstream: upstream, name: branch, type: "stale", reason: "checked out in CWD worktree" } else { action: :delete, branch: branch, upstream: upstream, name: branch, type: "stale", reason: "upstream gone" } end end orphan = orphan_local_branches( active_branch: active_branch, cwd_branch: cwd_branch ).map do |branch| if gh_available? tip_sha = begin git_capture!( "rev-parse", "--verify", branch ).strip rescue StandardError nil end if tip_sha merged_pr, = merged_pr_for_branch( branch: branch, branch_tip_sha: tip_sha ) if merged_pr.nil? && branch_absorbed_into_main?( branch: branch ) merged_pr = { url: "absorbed into #{config.main_branch}" } end if merged_pr { action: :delete, branch: branch, upstream: "", name: branch, type: "orphan", reason: "merged — #{merged_pr[ :url ]}" } else { action: :skip, branch: branch, upstream: "", name: branch, type: "orphan", reason: "no merged PR evidence" } end else { action: :skip, branch: branch, upstream: "", name: branch, type: "orphan", reason: "cannot read branch tip SHA" } end else { action: :skip, branch: branch, upstream: "", name: branch, type: "orphan", reason: "gh CLI not available" } end end absorbed = absorbed_local_branches( active_branch: active_branch, cwd_branch: cwd_branch ).map do |entry| branch = entry.fetch( :branch ) upstream = entry.fetch( :upstream ) if gh_available? && branch_has_open_pr?( branch: branch ) { action: :skip, branch: branch, upstream: upstream, name: branch, type: "absorbed", reason: "open PR exists" } else { action: :delete, branch: branch, upstream: upstream, name: branch, type: "absorbed", reason: "content already on main" } end end { stale: stale, orphan: orphan, absorbed: absorbed } end |
#realpath_safe(path) ⇒ Object
Resolves a path to its canonical form, tolerating non-existent paths. Preserves canonical parents for missing paths so deleted worktrees still compare equal to git’s recorded path (for example /tmp vs /private/tmp).
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/carson/runtime/local/worktree.rb', line 67 def realpath_safe( path ) File.realpath( path ) rescue Errno::ENOENT = File.( path ) missing_segments = [] candidate = until File.exist?( candidate ) || Dir.exist?( candidate ) parent = File.dirname( candidate ) break if parent == candidate missing_segments.unshift( File.basename( candidate ) ) candidate = parent end base = if File.exist?( candidate ) || Dir.exist?( candidate ) File.realpath( candidate ) else candidate end missing_segments.empty? ? base : File.join( base, *missing_segments ) end |
#refresh! ⇒ Object
Re-applies hooks, templates, and audit after upgrading Carson.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/carson/runtime/local/onboard.rb', line 36 def refresh! fingerprint_status = block_if_outsider_fingerprints! return fingerprint_status unless fingerprint_status.nil? unless inside_git_work_tree? puts_line "#{repo_root} is not a git repository." return EXIT_ERROR end if verbose? puts_verbose "" puts_verbose "[Refresh]" hook_status = prepare! return hook_status unless hook_status == EXIT_OK drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" } stale_count = template_superseded_present.count template_status = template_apply! return template_status unless template_status == EXIT_OK @template_sync_result = template_propagate!( drift_count: drift_count + stale_count ) audit_status = audit! if audit_status == EXIT_OK puts_line "OK: Carson refresh completed for #{repo_root}." elsif audit_status == EXIT_BLOCK puts_line "Refresh complete — some checks need attention. Run carson audit for details." end return audit_status end puts_line "Refresh" hook_status = with_captured_output { prepare! } return hook_status unless hook_status == EXIT_OK puts_line "Hooks installed (#{config.managed_hooks.count} hooks)." template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" } stale_count = template_superseded_present.count template_status = with_captured_output { template_apply! } return template_status unless template_status == EXIT_OK total_drift = template_drift_count + stale_count if total_drift.positive? puts_line "Templates applied (#{template_drift_count} updated, #{stale_count} removed)." else puts_line "Templates in sync." end @template_sync_result = template_propagate!( drift_count: total_drift ) audit_status = audit! puts_line "Refresh complete." audit_status end |
#refresh_all! ⇒ Object
Re-applies hooks, templates, and audit across all governed repositories. Checks each repo for safety (active worktrees, uncommitted changes) and marks unsafe repos as pending to avoid disrupting active work.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/carson/runtime/local/onboard.rb', line 93 def refresh_all! repos = config.govern_repos if repos.empty? puts_line "No governed repositories configured." puts_line " Run carson onboard in each repo to register." return EXIT_ERROR end pending_before = pending_repos_for( command: "refresh" ) if pending_before.any? puts_line "#{pending_before.length} repo#{plural_suffix( count: pending_before.length )} pending from previous run" end puts_line "" puts_line "Refresh all (#{repos.length} repo#{plural_suffix( count: repos.length )})" refreshed = 0 pending = 0 failed = 0 repos.each do |repo_path| repo_name = File.basename( repo_path ) unless Dir.exist?( repo_path ) puts_line "#{repo_name}: not found" record_batch_skip( command: "refresh", repo_path: repo_path, reason: "path not found" ) failed += 1 next end safety = portfolio_repo_safety( repo_path: repo_path ) unless safety.fetch( :safe ) reason = safety.fetch( :reasons ).join( ", " ) puts_line "#{repo_name}: PENDING (#{reason})" record_batch_skip( command: "refresh", repo_path: repo_path, reason: reason ) pending += 1 next end status = refresh_single_repo( repo_path: repo_path, repo_name: repo_name ) if status == EXIT_ERROR failed += 1 else clear_batch_success( command: "refresh", repo_path: repo_path ) refreshed += 1 end end puts_line "" parts = [ "#{refreshed} refreshed" ] parts << "#{pending} still pending (will retry on next run)" if pending.positive? parts << "#{failed} failed" if failed.positive? puts_line "Refresh all complete: #{parts.join( ', ' )}." failed.zero? && pending.zero? ? EXIT_OK : EXIT_ERROR end |
#sweep_stale_worktrees! ⇒ Object
Removes agent-owned worktrees whose branch content is already on main.
22 23 24 |
# File 'lib/carson/runtime/local/worktree.rb', line 22 def sweep_stale_worktrees! Worktree.sweep_stale!( runtime: self ) end |
#sync!(json_output: false) ⇒ Object
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/carson/runtime/local/sync.rb', line 6 def sync!( json_output: false ) fingerprint_status = block_if_outsider_fingerprints! return fingerprint_status unless fingerprint_status.nil? unless working_tree_clean? return sync_finish( result: sync_dirty_result, exit_code: EXIT_BLOCK, json_output: json_output ) end start_branch = current_branch switched = false sync_git!( "fetch", config.git_remote, "--prune", json_output: json_output ) if start_branch != config.main_branch sync_git!( "switch", config.main_branch, json_output: json_output ) switched = true end sync_git!( "pull", "--ff-only", config.git_remote, config.main_branch, json_output: json_output ) ahead_count, behind_count, error_text = main_sync_counts if error_text return sync_finish( result: { command: "sync", status: "block", error: "unable to verify main sync state (#{error_text})" }, exit_code: EXIT_BLOCK, json_output: json_output ) end if ahead_count.zero? && behind_count.zero? return sync_finish( result: { command: "sync", status: "ok", ahead: 0, behind: 0, main_branch: config.main_branch, remote: config.git_remote }, exit_code: EXIT_OK, json_output: json_output ) end sync_finish( 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" }, exit_code: EXIT_BLOCK, json_output: json_output ) ensure git_system!( "switch", start_branch ) if switched && branch_exists?( branch_name: start_branch ) end |
#sync_all! ⇒ Object
Syncs main branch across all governed repositories.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/carson/runtime/local/sync.rb', line 46 def sync_all! repos = config.govern_repos if repos.empty? puts_line "No governed repositories configured." puts_line " Run carson onboard in each repo to register." return EXIT_ERROR end puts_line "" puts_line "Sync all (#{repos.length} repo#{plural_suffix( count: repos.length )})" synced = 0 failed = 0 repos.each do |repo_path| repo_name = File.basename( repo_path ) unless Dir.exist?( repo_path ) puts_line "#{repo_name}: not found" record_batch_skip( command: "sync", repo_path: repo_path, reason: "path not found" ) failed += 1 next end begin scoped_runtime = build_scoped_runtime( repo_path: repo_path ) status = scoped_runtime.sync! if status == EXIT_OK puts_line "#{repo_name}: ok" unless verbose? clear_batch_success( command: "sync", repo_path: repo_path ) synced += 1 else puts_line "#{repo_name}: could not sync" unless verbose? record_batch_skip( command: "sync", repo_path: repo_path, reason: "sync failed" ) failed += 1 end rescue StandardError => exception puts_line "#{repo_name}: could not sync (#{exception.})" record_batch_skip( command: "sync", repo_path: repo_path, reason: exception. ) failed += 1 end end puts_line "" puts_line "Sync all complete: #{synced} synced, #{failed} failed." failed.zero? ? EXIT_OK : EXIT_ERROR end |
#template_apply!(push_prep: false) ⇒ Object
Applies managed template files as full-file writes from Carson sources. Also removes superseded files that are no longer part of the managed set.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/carson/runtime/local/template.rb', line 105 def template_apply!( push_prep: false ) fingerprint_status = block_if_outsider_fingerprints! return fingerprint_status unless fingerprint_status.nil? puts_verbose "" puts_verbose "[Template Sync Apply]" results = template_results stale = template_superseded_present applied = 0 results.each do |entry| if entry.fetch( :status ) == "error" puts_verbose "template_file: #{entry.fetch( :file )} status=error reason=#{entry.fetch( :reason )}" next end file_path = File.join( repo_root, entry.fetch( :file ) ) if entry.fetch( :status ) == "ok" puts_verbose "template_file: #{entry.fetch( :file )} status=ok reason=in_sync" next end FileUtils.mkdir_p( File.dirname( file_path ) ) File.write( file_path, entry.fetch( :applied_content ) ) puts_verbose "template_file: #{entry.fetch( :file )} status=updated reason=#{entry.fetch( :reason )}" applied += 1 end removed = 0 stale.each do |file| file_path = resolve_repo_path!( relative_path: file, label: "superseded file #{file}" ) File.delete( file_path ) puts_verbose "template_file: #{file} status=removed reason=superseded" removed += 1 end error_count = results.count { |entry| entry.fetch( :status ) == "error" } puts_verbose "template_apply_summary: updated=#{applied} removed=#{removed} error=#{error_count}" unless verbose? if applied.positive? || removed.positive? summary_parts = [] summary_parts << "#{applied} updated" if applied.positive? summary_parts << "#{removed} removed" if removed.positive? puts_line "Templates applied (#{summary_parts.join( ", " )})." else puts_line "Templates in sync." end end return EXIT_ERROR if error_count.positive? return EXIT_BLOCK if push_prep && push_prep_commit! EXIT_OK end |
#template_check! ⇒ Object
Read-only template drift check; returns block when managed files are output of sync.
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
# File 'lib/carson/runtime/local/template.rb', line 23 def template_check! fingerprint_status = block_if_outsider_fingerprints! return fingerprint_status unless fingerprint_status.nil? puts_verbose "" puts_verbose "[Template Sync Check]" results = template_results stale = template_superseded_present drift_count = results.count { |entry| entry.fetch( :status ) == "drift" } error_count = results.count { |entry| entry.fetch( :status ) == "error" } stale_count = stale.count results.each do |entry| puts_verbose "template_file: #{entry.fetch( :file )} status=#{entry.fetch( :status )} reason=#{entry.fetch( :reason )}" end stale.each { |file| puts_verbose "template_file: #{file} status=stale reason=superseded" } puts_verbose "template_summary: total=#{results.count} drift=#{drift_count} stale=#{stale_count} error=#{error_count}" unless verbose? if ( drift_count + stale_count ).positive? summary_parts = [] summary_parts << "#{drift_count} of #{results.count} drifted" if drift_count.positive? summary_parts << "#{stale_count} stale" if stale_count.positive? puts_line "Templates: #{summary_parts.join( ", " )}" results.select { |entry| entry.fetch( :status ) == "drift" }.each { |entry| puts_line " #{entry.fetch( :file )}" } stale.each { |file| puts_line " #{file} — superseded" } else puts_line "Templates: #{results.count} files in sync" end end return EXIT_ERROR if error_count.positive? ( drift_count + stale_count ).positive? ? EXIT_BLOCK : EXIT_OK end |
#template_check_all! ⇒ Object
Read-only template drift check across all governed repositories.
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/carson/runtime/local/template.rb', line 57 def template_check_all! repos = config.govern_repos if repos.empty? puts_line "No governed repositories configured." puts_line " Run carson onboard in each repo to register." return EXIT_ERROR end puts_line "" puts_line "Template check all (#{repos.length} repo#{plural_suffix( count: repos.length )})" in_sync = 0 drifted = 0 failed = 0 repos.each do |repo_path| repo_name = File.basename( repo_path ) unless Dir.exist?( repo_path ) puts_line "#{repo_name}: not found" record_batch_skip( command: "template_check", repo_path: repo_path, reason: "path not found" ) failed += 1 next end begin scoped_runtime = build_scoped_runtime( repo_path: repo_path ) status = scoped_runtime.template_check! if status == EXIT_OK puts_line "#{repo_name}: in sync" unless verbose? clear_batch_success( command: "template_check", repo_path: repo_path ) in_sync += 1 else puts_line "#{repo_name}: DRIFT" unless verbose? drifted += 1 end rescue StandardError => exception puts_line "#{repo_name}: could not complete (#{exception.})" record_batch_skip( command: "template_check", repo_path: repo_path, reason: exception. ) failed += 1 end end puts_line "" puts_line "Template check complete: #{in_sync} in sync, #{drifted} drifted, #{failed} failed." drifted.zero? && failed.zero? ? EXIT_OK : EXIT_BLOCK end |
#worktree_create!(name:, json_output: false) ⇒ Object
Creates a new worktree under .claude/worktrees/<name>.
12 13 14 |
# File 'lib/carson/runtime/local/worktree.rb', line 12 def worktree_create!( name:, json_output: false ) Worktree.create!( name: name, runtime: self, json_output: json_output ) end |
#worktree_list ⇒ Object
Returns all registered worktrees as Carson::Worktree instances.
27 28 29 |
# File 'lib/carson/runtime/local/worktree.rb', line 27 def worktree_list Worktree.list( runtime: self ) end |
#worktree_remove!(worktree_path:, force: false, json_output: false) ⇒ Object
Removes a worktree: directory, git registration, and branch.
17 18 19 |
# File 'lib/carson/runtime/local/worktree.rb', line 17 def worktree_remove!( worktree_path:, force: false, json_output: false ) Worktree.remove!( path: worktree_path, runtime: self, force: force, json_output: json_output ) end |