carson 2.7.0 → 2.8.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: fdf6978ed4ecdbef69aba67dba84b678d1145038ff47925a1dfbfdc777170135
4
- data.tar.gz: a79ce86bc59f79528e0bf73d51098cd8f1ef1c96c2034d98900c282e26d047c7
3
+ metadata.gz: 0c3f3bf2f0ea58a2d460c36fb57b6313d5dab97c08b76ec94ae1c1e9b685b24f
4
+ data.tar.gz: 48562197450233540c28a6fa912c38eaa636910ed3bab57e32e95d7b0b66c10a
5
5
  SHA512:
6
- metadata.gz: 5c11f2bd0fa18ca630b6c51bc23aae320e96bc63cfd24359c685b4664dd45594c68e3333554a9628c0411833de6c4a1779ef18cdd1d6b048516b116848a88484
7
- data.tar.gz: c6d3810f02303d1dbe6f614bc85b6d4a4186a171fa08b9705f66a86cac206eaf1f7c63e67c8a346d5ec9fd0a4c3454fbedbf5fed7275e46a9f1d03f5bd6d55fb
6
+ metadata.gz: 63a4d9fed728d78b2fcaf0d0d6ffb158a93e464af24997d661b24d77a2764e4a84014f45d4cfe2e204c7bac337c796f8e69e3722fe30a135f98650176271dba3
7
+ data.tar.gz: aba034301bccbf786198a1a44bd767d3f239370807a68841a89b6ffc40340719ef37dea2ddae9a0d54eedadf1b42c2446d02614721ab946586b809de0438a2d4
@@ -66,13 +66,6 @@ jobs:
66
66
  - name: Install pinned RuboCop
67
67
  run: gem install rubocop -v "${{ inputs.rubocop_version }}" --no-document
68
68
 
69
- - name: Align remote name for Carson
70
- working-directory: host
71
- run: |
72
- if ! git remote get-url github >/dev/null 2>&1; then
73
- git remote rename origin github
74
- fi
75
-
76
69
  - name: Carson audit
77
70
  working-directory: host
78
71
  env:
data/API.md CHANGED
@@ -15,8 +15,9 @@ carson <command> [subcommand] [arguments]
15
15
 
16
16
  | Command | Purpose |
17
17
  |---|---|
18
+ | `carson setup` | Interactive quiz to configure remote, main branch, workflow, and merge method. Writes `~/.carson/config.json`. |
18
19
  | `carson lint setup --source <path-or-git-url> [--ref <git-ref>] [--force]` | Seed or refresh `~/.carson/lint` policy files from an explicit source. |
19
- | `carson onboard [repo_path]` | Apply one-command baseline setup for a target git repository. |
20
+ | `carson onboard [repo_path]` | Apply one-command baseline setup for a target git repository. Auto-triggers `setup` on first run. |
20
21
  | `carson prepare` | Install or refresh Carson-managed global hooks. |
21
22
  | `carson refresh [repo_path]` | Re-apply hooks, templates, and audit after upgrading Carson. |
22
23
  | `carson offboard [repo_path]` | Remove Carson-managed host artefacts and detach Carson hooks path where applicable. |
data/MANUAL.md CHANGED
@@ -50,14 +50,25 @@ Policy layout: language config files sit directly under `CODING/` (flat layout,
50
50
  carson onboard /path/to/your-repo
51
51
  ```
52
52
 
53
+ On first run (no `~/.carson/config.json` exists), `onboard` launches `carson setup` — an interactive quiz that detects your remotes, main branch, and preferred workflow. In non-interactive environments (CI, pipes), Carson auto-detects settings silently.
54
+
53
55
  `onboard` performs:
54
- - Remote alignment using configured `git.remote` (default `github`).
56
+ - Interactive setup quiz (first run only).
57
+ - Remote detection and verification using configured `git.remote` (default `origin`).
55
58
  - Hook installation under `~/.carson/hooks/<version>/`.
56
59
  - Repository `core.hooksPath` alignment to Carson global hooks.
57
60
  - Commit-time governance gate via managed `pre-commit` hook.
58
61
  - Managed `.github/*` template synchronisation.
59
62
  - Initial governance audit.
60
63
 
64
+ ### Reconfigure later
65
+
66
+ ```bash
67
+ carson setup
68
+ ```
69
+
70
+ Re-run the interactive setup quiz to change your remote, main branch, workflow style, or merge method. Choices are saved to `~/.carson/config.json`.
71
+
61
72
  ### Step 3: Commit generated files
62
73
 
63
74
  After `onboard`, commit the generated `.github/*` changes in your repository. From this point the repository is governed.
data/RELEASE.md CHANGED
@@ -5,6 +5,67 @@ 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
+ ## 2.8.0 — Interactive Setup and Remote Detection
9
+
10
+ ### What changed
11
+
12
+ - **`carson setup` command.** An interactive quiz that detects git remotes, main branch, workflow style, and merge method. Writes answers to `~/.carson/config.json`. In non-TTY environments, Carson auto-detects settings silently.
13
+ - **Auto-triggered on first onboard.** `carson onboard` now launches the setup quiz when no `~/.carson/config.json` exists. Existing users are not affected.
14
+ - **Remote renaming removed.** Carson no longer renames `origin` to `github` during onboard. Instead, it detects the existing remote and adapts. This respects the user's repository layout.
15
+ - **Default remote changed from `github` to `origin`.** The built-in default `git.remote` is now `origin`, matching the convention of most git hosting providers. Users who previously relied on the `github` default should run `carson setup` or set `git.remote` in config.
16
+ - **Post-install message.** `gem install carson` now displays a getting-started guide pointing to `carson onboard`.
17
+ - **CI lint fallback simplified.** `lint_target_files_for_pull_request` uses `config.git_remote` directly with a minimal fallback to `origin`.
18
+
19
+ ### What users must do now
20
+
21
+ 1. Upgrade Carson to `2.8.0`.
22
+ 2. If you relied on the `github` remote default, either rename your remote to `origin` or run `carson setup` to configure `git.remote`.
23
+
24
+ ### Breaking or removed behaviour
25
+
26
+ - `git.remote` default changed from `github` to `origin`.
27
+ - `carson onboard` no longer renames `origin` to `github`.
28
+ - The `align_remote_name_for_carson!` method has been removed.
29
+
30
+ ### Upgrade steps
31
+
32
+ ```bash
33
+ cd ~/Dev/carson
34
+ git pull
35
+ bash install.sh
36
+ carson version
37
+ carson setup
38
+ ```
39
+
40
+ ### Engineering Appendix
41
+
42
+ #### New files
43
+
44
+ - `lib/carson/runtime/setup.rb` — interactive quiz, remote/branch detection, config persistence.
45
+ - `test/runtime_setup_test.rb` — quiz, detection, and config merge tests.
46
+
47
+ #### Modified components
48
+
49
+ - `lib/carson/config.rb` — default `git.remote` changed from `"github"` to `"origin"`, added `attr_accessor :git_remote`.
50
+ - `lib/carson/runtime.rb` — added `in_stream:` parameter and `@in` attribute.
51
+ - `lib/carson/cli.rb` — added `"setup"` command dispatch, updated banner.
52
+ - `lib/carson/runtime/local.rb` — replaced `align_remote_name_for_carson!` with `report_detected_remote!`, updated `onboard!` to auto-trigger setup, updated `print_onboarding_guidance`.
53
+ - `lib/carson/runtime/audit.rb` — simplified `lint_target_files_for_pull_request` CI fallback.
54
+ - `test/runtime_govern_test.rb` — removed `origin` → `github` rename.
55
+ - `test/runtime_audit_lint_test.rb` — updated remote name from `github` to `origin`.
56
+ - `carson.gemspec` — added `spec.post_install_message`.
57
+ - `MANUAL.md` — documented `carson setup`, updated remote default.
58
+ - `API.md` — added `setup` command entry.
59
+
60
+ #### Public interface and config changes
61
+
62
+ - Added CLI command: `carson setup`.
63
+ - Default `git.remote` changed from `"github"` to `"origin"`.
64
+ - Runtime constructor accepts `in_stream:` keyword argument.
65
+ - Exit status contract unchanged.
66
+
67
+ ---
68
+
8
69
  ## 2.7.0 — Documentation and Test Fixes
9
70
 
10
71
  ### What changed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.7.0
1
+ 2.8.0
data/carson.gemspec CHANGED
@@ -19,6 +19,18 @@ Gem::Specification.new do |spec|
19
19
  "documentation_uri" => "https://github.com/wanghailei/carson/blob/main/MANUAL.md"
20
20
  }
21
21
 
22
+ spec.post_install_message = <<~MSG
23
+
24
+ \u29D3 Carson at your service.
25
+
26
+ Step into your project directory and run:
27
+
28
+ carson onboard
29
+
30
+ I'll walk you through everything from there.
31
+
32
+ MSG
33
+
22
34
  spec.bindir = "exe"
23
35
  spec.executables = [ "carson" ]
24
36
  spec.require_paths = [ "lib" ]
data/lib/carson/cli.rb CHANGED
@@ -44,7 +44,7 @@ module Carson
44
44
 
45
45
  def self.build_parser
46
46
  OptionParser.new do |opts|
47
- opts.banner = "Usage: carson [audit|sync|prune|prepare|inspect|onboard [repo_path]|refresh [repo_path]|offboard [repo_path]|template check|template apply|lint setup --source <path-or-git-url>|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|housekeep|version]"
47
+ opts.banner = "Usage: carson [setup|audit|sync|prune|prepare|inspect|onboard [repo_path]|refresh [repo_path]|offboard [repo_path]|template check|template apply|lint setup --source <path-or-git-url>|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|housekeep|version]"
48
48
  end
49
49
  end
50
50
 
@@ -187,6 +187,8 @@ module Carson
187
187
  return Runtime::EXIT_ERROR if command == :invalid
188
188
 
189
189
  case command
190
+ when "setup"
191
+ runtime.setup!
190
192
  when "audit"
191
193
  runtime.audit!
192
194
  when "sync"
data/lib/carson/config.rb CHANGED
@@ -6,7 +6,8 @@ module Carson
6
6
 
7
7
  # Config is built-in only for outsider mode; host repositories do not carry Carson config files.
8
8
  class Config
9
- attr_reader :git_remote, :main_branch, :protected_branches, :hooks_base_path, :required_hooks,
9
+ attr_accessor :git_remote
10
+ attr_reader :main_branch, :protected_branches, :hooks_base_path, :required_hooks,
10
11
  :path_groups, :template_managed_files, :lint_languages,
11
12
  :review_wait_seconds, :review_poll_seconds, :review_max_polls, :review_sweep_window_days,
12
13
  :review_sweep_states, :review_disposition_prefix, :review_risk_keywords,
@@ -28,7 +29,7 @@ module Carson
28
29
  def self.default_data
29
30
  {
30
31
  "git" => {
31
- "remote" => "github",
32
+ "remote" => "origin",
32
33
  "main_branch" => "main",
33
34
  "protected_branches" => [ "main", "master" ]
34
35
  },
@@ -205,19 +205,11 @@ module Carson
205
205
  base_ref = ENV.fetch( "GITHUB_BASE_REF", "" ).to_s.strip
206
206
  return nil if base_ref.empty?
207
207
 
208
- preferred_remote = config.git_remote.to_s.strip
209
- remote_name = nil
210
-
211
- remotes_stdout, _, remotes_success, = git_run( "remote" )
212
- if remotes_success
213
- available_remotes = remotes_stdout.lines.map { |line| line.to_s.strip }.reject( &:empty? )
214
- candidates = [ preferred_remote, "origin", "github" ].map( &:to_s ).map( &:strip ).reject( &:empty? ).uniq
215
- remote_name = candidates.find { |candidate| available_remotes.include?( candidate ) }
216
- remote_name ||= available_remotes.first unless available_remotes.empty?
208
+ remote_name = config.git_remote
209
+ unless git_remote_exists?( remote_name: remote_name )
210
+ remote_name = "origin" if git_remote_exists?( remote_name: "origin" )
217
211
  end
218
212
 
219
- remote_name ||= ( preferred_remote.empty? ? "origin" : preferred_remote )
220
-
221
213
  _, _, fetch_success, = git_run( "fetch", "--no-tags", "--depth", "1", remote_name, base_ref )
222
214
  return nil unless fetch_success
223
215
 
@@ -161,7 +161,7 @@ module Carson
161
161
  inspect!
162
162
  end
163
163
 
164
- # One-command onboarding for new repositories: align remote naming, install hooks,
164
+ # One-command onboarding for new repositories: detect remote, install hooks,
165
165
  # apply templates, and produce a first audit report.
166
166
  def onboard!
167
167
  fingerprint_status = block_if_outsider_fingerprints!
@@ -172,7 +172,17 @@ module Carson
172
172
  puts_line "ERROR: #{repo_root} is not a git repository."
173
173
  return EXIT_ERROR
174
174
  end
175
- align_remote_name_for_carson!
175
+
176
+ unless global_config_exists?
177
+ if self.in.respond_to?( :tty? ) && self.in.tty?
178
+ setup_status = setup!
179
+ return setup_status unless setup_status == EXIT_OK
180
+ else
181
+ silent_setup!
182
+ end
183
+ end
184
+
185
+ report_detected_remote!
176
186
  hook_status = prepare!
177
187
  return hook_status unless hook_status == EXIT_OK
178
188
 
@@ -661,30 +671,20 @@ module Carson
661
671
  end
662
672
  end
663
673
 
664
- # Ensures Carson expected remote naming (`github`) while keeping existing
665
- # repositories safe when neither `github` nor `origin` exists.
666
- def align_remote_name_for_carson!
674
+ # Verifies configured remote exists and logs status without mutating remotes.
675
+ def report_detected_remote!
667
676
  if git_remote_exists?( remote_name: config.git_remote )
668
677
  puts_line "remote_ok: #{config.git_remote}"
669
- return
670
- end
671
- if git_remote_exists?( remote_name: "origin" )
672
- git_system!( "remote", "rename", "origin", config.git_remote )
673
- puts_line "remote_renamed: origin -> #{config.git_remote}"
674
- return
678
+ else
679
+ puts_line "WARN: remote '#{config.git_remote}' not found; run carson setup to configure."
675
680
  end
676
- puts_line "WARN: no #{config.git_remote} or origin remote configured; continue with local baseline only."
677
681
  end
678
682
 
679
-
680
683
  def print_onboarding_guidance
681
684
  puts_line ""
682
685
  puts_line "Carson is ready. Current workflow: #{config.workflow_style}"
683
686
  puts_line ""
684
- puts_line "Customise in ~/.carson/config.json:"
685
- puts_line " { \"workflow\": { \"style\": \"branch\" } } — enforce PR-only merges"
686
- puts_line " { \"workflow\": { \"style\": \"trunk\" } } — allow direct main commits (default)"
687
- puts_line ""
687
+ puts_line "Reconfigure anytime with: carson setup"
688
688
  puts_line "Run carson refresh after changing config."
689
689
  end
690
690
 
@@ -0,0 +1,295 @@
1
+ module Carson
2
+ class Runtime
3
+ module Setup
4
+ WELL_KNOWN_REMOTES = %w[origin github upstream].freeze
5
+
6
+ def setup!
7
+ print_header "Setup"
8
+
9
+ unless inside_git_work_tree?
10
+ puts_line "WARN: not a git repository. Skipping remote and branch detection."
11
+ return write_setup_config( choices: {} )
12
+ end
13
+
14
+ if self.in.respond_to?( :tty? ) && self.in.tty?
15
+ interactive_setup!
16
+ else
17
+ silent_setup!
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def interactive_setup!
24
+ choices = {}
25
+
26
+ remote_choice = prompt_remote
27
+ choices[ "git.remote" ] = remote_choice unless remote_choice.nil?
28
+
29
+ branch_choice = prompt_main_branch
30
+ choices[ "git.main_branch" ] = branch_choice unless branch_choice.nil?
31
+
32
+ workflow_choice = prompt_workflow_style
33
+ choices[ "workflow.style" ] = workflow_choice unless workflow_choice.nil?
34
+
35
+ merge_choice = prompt_merge_method
36
+ choices[ "govern.merge.method" ] = merge_choice unless merge_choice.nil?
37
+
38
+ write_setup_config( choices: choices )
39
+ end
40
+
41
+ def silent_setup!
42
+ detected = detect_git_remote
43
+ choices = {}
44
+ if detected && detected != config.git_remote
45
+ choices[ "git.remote" ] = detected
46
+ puts_line "detected_remote: #{detected}"
47
+ elsif detected
48
+ puts_line "detected_remote: #{detected}"
49
+ else
50
+ puts_line "detected_remote: none"
51
+ end
52
+
53
+ branch = detect_main_branch
54
+ if branch && branch != config.main_branch
55
+ choices[ "git.main_branch" ] = branch
56
+ puts_line "detected_main_branch: #{branch}"
57
+ elsif branch
58
+ puts_line "detected_main_branch: #{branch}"
59
+ end
60
+
61
+ write_setup_config( choices: choices )
62
+ end
63
+
64
+ def prompt_remote
65
+ remotes = list_git_remotes
66
+ if remotes.empty?
67
+ puts_line "No remotes found. Carson will operate in local-only mode."
68
+ return nil
69
+ end
70
+
71
+ puts_line ""
72
+ puts_line "Git remote"
73
+ options = build_remote_options( remotes: remotes )
74
+ options << { label: "Other (enter name)", value: :other }
75
+
76
+ default_index = 0
77
+ choice = prompt_choice( options: options, default: default_index )
78
+
79
+ if choice == :other
80
+ prompt_custom_value( label: "Remote name" )
81
+ else
82
+ choice
83
+ end
84
+ end
85
+
86
+ def prompt_main_branch
87
+ puts_line ""
88
+ puts_line "Main branch"
89
+ options = build_main_branch_options
90
+ options << { label: "Other (enter name)", value: :other }
91
+
92
+ default_index = 0
93
+ choice = prompt_choice( options: options, default: default_index )
94
+
95
+ if choice == :other
96
+ prompt_custom_value( label: "Branch name" )
97
+ else
98
+ choice
99
+ end
100
+ end
101
+
102
+ def prompt_workflow_style
103
+ puts_line ""
104
+ puts_line "Workflow style"
105
+ options = [
106
+ { label: "trunk — commit directly to main (default)", value: "trunk" },
107
+ { label: "branch — enforce PR-only merges", value: "branch" }
108
+ ]
109
+ prompt_choice( options: options, default: 0 )
110
+ end
111
+
112
+ def prompt_merge_method
113
+ puts_line ""
114
+ puts_line "Merge method"
115
+ options = [
116
+ { label: "squash — one commit per PR (recommended)", value: "squash" },
117
+ { label: "rebase — linear history, individual commits", value: "rebase" },
118
+ { label: "merge — merge commits", value: "merge" }
119
+ ]
120
+ prompt_choice( options: options, default: 0 )
121
+ end
122
+
123
+ def prompt_choice( options:, default: )
124
+ options.each_with_index do |option, index|
125
+ puts_line " #{index + 1}) #{option.fetch( :label )}"
126
+ end
127
+ out.print "#{BADGE} Choice [#{default + 1}]: "
128
+ out.flush
129
+ raw = self.in.gets
130
+ return options[ default ].fetch( :value ) if raw.nil?
131
+
132
+ input = raw.to_s.strip
133
+ return options[ default ].fetch( :value ) if input.empty?
134
+
135
+ index = Integer( input ) - 1
136
+ return options[ default ].fetch( :value ) if index < 0 || index >= options.length
137
+
138
+ options[ index ].fetch( :value )
139
+ rescue ArgumentError
140
+ options[ default ].fetch( :value )
141
+ end
142
+
143
+ def prompt_custom_value( label: )
144
+ out.print "#{BADGE} #{label}: "
145
+ out.flush
146
+ raw = self.in.gets
147
+ return nil if raw.nil?
148
+
149
+ value = raw.to_s.strip
150
+ value.empty? ? nil : value
151
+ end
152
+
153
+ def build_remote_options( remotes: )
154
+ sorted = sort_remotes( remotes: remotes )
155
+ sorted.map do |entry|
156
+ name = entry.fetch( :name )
157
+ url = entry.fetch( :url )
158
+ { label: "#{name} (#{url})", value: name }
159
+ end
160
+ end
161
+
162
+ def sort_remotes( remotes: )
163
+ well_known = []
164
+ others = []
165
+ remotes.each do |entry|
166
+ if WELL_KNOWN_REMOTES.include?( entry.fetch( :name ) )
167
+ well_known << entry
168
+ else
169
+ others << entry
170
+ end
171
+ end
172
+ well_known.sort_by { |e| WELL_KNOWN_REMOTES.index( e.fetch( :name ) ) || 999 } + others.sort_by { |e| e.fetch( :name ) }
173
+ end
174
+
175
+ def build_main_branch_options
176
+ options = []
177
+ main_exists = branch_exists_locally_or_remote?( branch: "main" )
178
+ master_exists = branch_exists_locally_or_remote?( branch: "master" )
179
+
180
+ if main_exists
181
+ options << { label: "main", value: "main" }
182
+ options << { label: "master", value: "master" } if master_exists
183
+ elsif master_exists
184
+ options << { label: "master", value: "master" }
185
+ options << { label: "main", value: "main" }
186
+ else
187
+ options << { label: "main", value: "main" }
188
+ options << { label: "master", value: "master" }
189
+ end
190
+ options
191
+ end
192
+
193
+ def branch_exists_locally_or_remote?( branch: )
194
+ return true if branch_exists?( branch_name: branch )
195
+
196
+ remote = config.git_remote
197
+ _, _, success, = git_run( "rev-parse", "--verify", "#{remote}/#{branch}" )
198
+ success
199
+ end
200
+
201
+ def list_git_remotes
202
+ stdout_text, _, success, = git_run( "remote", "-v" )
203
+ return [] unless success
204
+
205
+ remotes = {}
206
+ stdout_text.lines.each do |line|
207
+ parts = line.strip.split( /\s+/ )
208
+ next if parts.length < 2
209
+
210
+ name = parts[ 0 ]
211
+ url = parts[ 1 ]
212
+ remotes[ name ] ||= url
213
+ end
214
+ remotes.map { |name, url| { name: name, url: url } }
215
+ end
216
+
217
+ def detect_git_remote
218
+ remotes = list_git_remotes
219
+ remote_names = remotes.map { |entry| entry.fetch( :name ) }
220
+ return nil if remote_names.empty?
221
+
222
+ return config.git_remote if remote_names.include?( config.git_remote )
223
+ return remote_names.first if remote_names.length == 1
224
+
225
+ candidate = WELL_KNOWN_REMOTES.find { |name| remote_names.include?( name ) }
226
+ return candidate unless candidate.nil?
227
+
228
+ remote_names.first
229
+ end
230
+
231
+ def detect_main_branch
232
+ return "main" if branch_exists_locally_or_remote?( branch: "main" )
233
+ return "master" if branch_exists_locally_or_remote?( branch: "master" )
234
+
235
+ nil
236
+ end
237
+
238
+ def write_setup_config( choices: )
239
+ config_data = build_config_data_from_choices( choices: choices )
240
+
241
+ config_path = Config.global_config_path( repo_root: repo_root )
242
+ if config_path.empty?
243
+ puts_line "WARN: unable to determine config path; skipping config write."
244
+ return EXIT_OK
245
+ end
246
+
247
+ existing_data = load_existing_config( path: config_path )
248
+ merged = Config.deep_merge( base: existing_data, overlay: config_data )
249
+
250
+ FileUtils.mkdir_p( File.dirname( config_path ) )
251
+ File.write( config_path, JSON.pretty_generate( merged ) )
252
+ puts_line ""
253
+ puts_line "Config saved to #{config_path}"
254
+
255
+ reload_config_after_setup!
256
+ EXIT_OK
257
+ end
258
+
259
+ def build_config_data_from_choices( choices: )
260
+ data = {}
261
+ choices.each do |key, value|
262
+ next if value.nil?
263
+
264
+ parts = key.split( "." )
265
+ current = data
266
+ parts[ 0..-2 ].each do |part|
267
+ current[ part ] ||= {}
268
+ current = current[ part ]
269
+ end
270
+ current[ parts.last ] = value
271
+ end
272
+ data
273
+ end
274
+
275
+ def load_existing_config( path: )
276
+ return {} unless File.file?( path )
277
+
278
+ JSON.parse( File.read( path ) )
279
+ rescue JSON::ParserError
280
+ {}
281
+ end
282
+
283
+ def reload_config_after_setup!
284
+ @config = Config.load( repo_root: repo_root )
285
+ end
286
+
287
+ def global_config_exists?
288
+ path = Config.global_config_path( repo_root: repo_root )
289
+ !path.empty? && File.file?( path )
290
+ end
291
+ end
292
+
293
+ include Setup
294
+ end
295
+ end
@@ -22,11 +22,12 @@ module Carson
22
22
  DISPOSITION_TOKENS = %w[accepted rejected deferred].freeze
23
23
 
24
24
  # Runtime wiring for repository context, tool paths, and output streams.
25
- def initialize( repo_root:, tool_root:, out:, err: )
25
+ def initialize( repo_root:, tool_root:, out:, err:, in_stream: $stdin )
26
26
  @repo_root = repo_root
27
27
  @tool_root = tool_root
28
28
  @out = out
29
29
  @err = err
30
+ @in = in_stream
30
31
  @config = Config.load( repo_root: repo_root )
31
32
  @git_adapter = Adapters::Git.new( repo_root: repo_root )
32
33
  @github_adapter = Adapters::GitHub.new( repo_root: repo_root )
@@ -34,7 +35,7 @@ module Carson
34
35
 
35
36
  private
36
37
 
37
- attr_reader :repo_root, :tool_root, :out, :err, :config, :git_adapter, :github_adapter
38
+ attr_reader :repo_root, :tool_root, :out, :err, :in, :config, :git_adapter, :github_adapter
38
39
 
39
40
  # Current local branch name.
40
41
  def current_branch
@@ -187,3 +188,4 @@ require_relative "runtime/lint"
187
188
  require_relative "runtime/audit"
188
189
  require_relative "runtime/review"
189
190
  require_relative "runtime/govern"
191
+ require_relative "runtime/setup"
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: 2.7.0
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang
@@ -56,6 +56,7 @@ files:
56
56
  - lib/carson/runtime/review/query_text.rb
57
57
  - lib/carson/runtime/review/sweep_support.rb
58
58
  - lib/carson/runtime/review/utility.rb
59
+ - lib/carson/runtime/setup.rb
59
60
  - lib/carson/version.rb
60
61
  - templates/.github/AGENTS.md
61
62
  - templates/.github/CLAUDE.md
@@ -70,6 +71,16 @@ metadata:
70
71
  changelog_uri: https://github.com/wanghailei/carson/blob/main/RELEASE.md
71
72
  bug_tracker_uri: https://github.com/wanghailei/carson/issues
72
73
  documentation_uri: https://github.com/wanghailei/carson/blob/main/MANUAL.md
74
+ post_install_message: |2+
75
+
76
+ ⧓ Carson at your service.
77
+
78
+ Step into your project directory and run:
79
+
80
+ carson onboard
81
+
82
+ I'll walk you through everything from there.
83
+
73
84
  rdoc_options: []
74
85
  require_paths:
75
86
  - lib
@@ -88,3 +99,4 @@ rubygems_version: 4.0.3
88
99
  specification_version: 4
89
100
  summary: Outsider governance runtime for repository hygiene and merge readiness.
90
101
  test_files: []
102
+ ...