carson 2.6.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: cd6dd1f0f0879b5838bc8d808261e44c408b4796ca85dcdd96f6885e397fd109
4
- data.tar.gz: 0313e015a232d3eb29b46c16567457ac5403d83f0bcd062f42d4a9efd374d22b
3
+ metadata.gz: 0c3f3bf2f0ea58a2d460c36fb57b6313d5dab97c08b76ec94ae1c1e9b685b24f
4
+ data.tar.gz: 48562197450233540c28a6fa912c38eaa636910ed3bab57e32e95d7b0b66c10a
5
5
  SHA512:
6
- metadata.gz: 7932b95b9798bd6935e5fc1f4f21962e31ebc473863e802f3870691e1613e00eaef5daa035239f6041d45750a6fb3fd85d29a8b72aa95285edcfbf9922992707
7
- data.tar.gz: 5ed203aa2b53d844cd71b2496c45de3abed272ce765d590e7695314e29c8b2affbe79cd59ee86286adaa1118696438448b0079da97107e30dd5d5d21fb0b83c2
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. |
@@ -40,7 +41,7 @@ carson <command> [subcommand] [arguments]
40
41
 
41
42
  `--loop SECONDS` runs the govern cycle continuously, sleeping SECONDS between cycles. The loop isolates errors per cycle — a single failing cycle does not stop the daemon. `Ctrl-C` cleanly exits with a cycle count summary. SECONDS must be a positive integer.
42
43
 
43
- `govern.merge.method` accepts `squash`, `merge`, or `rebase` (default: `squash`). Squash keeps main linear — one PR, one commit. When the target repository enforces linear history via branch protection, only `rebase` is accepted by GitHub — set `govern.merge.method` to `rebase` to match.
44
+ `govern.merge.method` accepts `squash`, `merge`, or `rebase` (default: `squash`). Squash keeps main linear — one PR, one commit. When the target repository enforces linear history via branch protection, both `squash` and `rebase` are accepted by GitHub — only `merge` is rejected.
44
45
 
45
46
  ### Review commands
46
47
 
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.
@@ -160,7 +171,7 @@ Carson's `govern.merge.method` controls how `carson govern` merges ready PRs. Th
160
171
 
161
172
  **When to use other methods:**
162
173
 
163
- - `rebase` — if you want to preserve individual commits from the branch on main. Requires "Require linear history" in GitHub branch protection. GitHub rejects merge commits and squash merges when this is enabled.
174
+ - `rebase` — if you want to preserve individual commits from the branch on main. Both `squash` and `rebase` are compatible with GitHub's "Require linear history" branch protection only `merge` is rejected.
164
175
  - `merge` — if you want explicit merge commits. This creates a non-linear graph but preserves branch topology.
165
176
 
166
177
  **Important:** Carson's merge method must match your GitHub repository's allowed merge types. If your repo only allows squash merges and Carson is set to `merge`, govern will fail when it tries to auto-merge. Check your repository settings under Settings > General > Pull Requests.
data/README.md CHANGED
@@ -43,7 +43,7 @@ This separation is Carson's defining trait — the **outsider boundary**: no Car
43
43
  The data flow:
44
44
 
45
45
  1. You maintain a **policy source** — a directory or git repository containing your lint rules (e.g. `CODING/rubocop.yml`). Carson copies these to `~/.carson/lint/` via `carson lint setup`.
46
- 2. `carson init` installs git hooks, synchronises `.github/*` templates, and runs a first governance audit on a host repository.
46
+ 2. `carson onboard` installs git hooks, synchronises `.github/*` templates, and runs a first governance audit on a host repository.
47
47
  3. From that point, every commit triggers `carson audit` through the managed `pre-commit` hook. The same `carson audit` runs in GitHub Actions. If it passes locally, it passes in CI.
48
48
  4. `carson review gate` enforces review accountability: it blocks merge until every actionable reviewer comment has been formally acknowledged by the PR author through a **disposition comment**.
49
49
  5. `carson govern` triages all open PRs across your portfolio. Ready PRs are merged and housekept. Failing PRs get a coding agent dispatched to fix them. Stuck PRs are escalated for your attention.
data/RELEASE.md CHANGED
@@ -5,25 +5,148 @@ 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.6.0 — Default Squash Merge + Agent Discovery Templates
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
+
69
+ ## 2.7.0 — Documentation and Test Fixes
70
+
71
+ ### What changed
72
+
73
+ - **Stale command reference fixed.** README.md referenced the pre-2.3.0 command name `carson init` instead of `carson onboard`.
74
+ - **Linear history guidance corrected.** API.md and MANUAL.md incorrectly stated that GitHub's "Require linear history" only accepts rebase merges. Both squash and rebase are accepted — only merge commits are rejected.
75
+ - **Release notes separated.** The combined 2.6.0 entry has been split into distinct 2.5.0 (agent discovery) and 2.6.0 (squash default) entries.
76
+ - **Config default test made hermetic.** `test_config_govern_defaults` now isolates HOME to a temp directory, preventing the developer's local `~/.carson/config.json` from affecting test results.
77
+
78
+ ### What users must do now
79
+
80
+ 1. Upgrade Carson to `2.7.0`.
81
+
82
+ ### Breaking or removed behaviour
83
+
84
+ - None.
85
+
86
+ ### Upgrade steps
87
+
88
+ ```bash
89
+ cd ~/Dev/carson
90
+ git pull
91
+ bash install.sh
92
+ carson version
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 2.6.0 — Default Squash Merge
9
98
 
10
99
  ### What changed
11
100
 
12
101
  - **Default merge method changed from `merge` to `squash`.** Squash-to-main keeps history linear: one PR = one commit on main. Every commit on main corresponds to a reviewed, CI-passing unit of work and is individually revertable. This aligns Carson's built-in default with how most teams should run.
102
+
103
+ ### What users must do now
104
+
105
+ 1. Upgrade Carson to `2.6.0`.
106
+ 2. If you previously set `govern.merge.method` to `"merge"` explicitly in `~/.carson/config.json`, review whether `"squash"` (now the default) is the right choice.
107
+
108
+ ### Breaking or removed behaviour
109
+
110
+ - `govern.merge.method` default changed from `merge` to `squash`. If your GitHub repository only allows merge commits, set `"govern": { "merge": { "method": "merge" } }` in `~/.carson/config.json`.
111
+
112
+ ### Upgrade steps
113
+
114
+ ```bash
115
+ cd ~/Dev/carson
116
+ git pull
117
+ bash install.sh
118
+ carson version
119
+ ```
120
+
121
+ ### Engineering Appendix
122
+
123
+ #### Modified components
124
+
125
+ - `lib/carson/config.rb` — `govern.merge.method` default changed from `"merge"` to `"squash"`.
126
+ - `test/runtime_govern_test.rb` — unit test updated for squash default.
127
+
128
+ #### Verification evidence
129
+
130
+ - CI passes on PR #78.
131
+
132
+ ---
133
+
134
+ ## 2.5.0 — Agent Discovery Templates
135
+
136
+ ### What changed
137
+
13
138
  - **Agent discovery via managed templates.** Interactive agents (Claude Code, Codex, Copilot) working in Carson-governed repos now discover Carson automatically. A new source-of-truth file `.github/carson-instructions.md` contains the full governance baseline. Agent-specific files (`.github/CLAUDE.md`, `.github/AGENTS.md`, `.github/copilot-instructions.md`) are one-line pointers to it. Zero drift risk — one file to maintain, all agents follow the same reference.
14
139
  - **Managed template set expanded.** `carson template apply` now writes five files: `carson-instructions.md`, `copilot-instructions.md`, `CLAUDE.md`, `AGENTS.md`, and `pull_request_template.md`.
15
140
 
16
141
  ### What users must do now
17
142
 
18
- 1. Upgrade Carson to `2.6.0`.
143
+ 1. Upgrade Carson to `2.5.0`.
19
144
  2. Run `carson prepare` in each governed repository.
20
145
  3. Run `carson template apply` to write the new managed files.
21
146
  4. Commit the new `.github/*` files.
22
- 5. If you previously set `govern.merge.method` to `"merge"` explicitly in `~/.carson/config.json`, review whether `"squash"` (now the default) is the right choice.
23
147
 
24
148
  ### Breaking or removed behaviour
25
149
 
26
- - `govern.merge.method` default changed from `merge` to `squash`. If your GitHub repository only allows merge commits, set `"govern": { "merge": { "method": "merge" } }` in `~/.carson/config.json`.
27
150
  - `.github/copilot-instructions.md` content replaced with a one-line reference. The governance baseline now lives in `.github/carson-instructions.md`.
28
151
 
29
152
  ### Upgrade steps
@@ -39,12 +162,6 @@ carson template apply
39
162
 
40
163
  ### Engineering Appendix
41
164
 
42
- #### Modified components
43
-
44
- - `lib/carson/config.rb` — `govern.merge.method` default changed from `"merge"` to `"squash"`; `template.managed_files` expanded to include `carson-instructions.md`, `CLAUDE.md`, and `AGENTS.md`.
45
- - `script/ci_smoke.sh` — offboard removal check updated for new managed files.
46
- - `test/runtime_govern_test.rb` — unit test updated for squash default.
47
-
48
165
  #### New files
49
166
 
50
167
  - `templates/.github/carson-instructions.md` — governance baseline source of truth.
@@ -55,15 +172,19 @@ carson template apply
55
172
 
56
173
  - `templates/.github/copilot-instructions.md` — replaced full content with one-line reference.
57
174
 
175
+ #### Modified components
176
+
177
+ - `lib/carson/config.rb` — `template.managed_files` expanded to include `carson-instructions.md`, `CLAUDE.md`, and `AGENTS.md`.
178
+ - `script/ci_smoke.sh` — offboard removal check updated for new managed files.
179
+
58
180
  #### Public interface and config changes
59
181
 
60
- - `govern.merge.method` default: `"merge"` → `"squash"`.
61
182
  - `template.managed_files` default expanded from 2 to 5 files.
62
183
  - Exit status contract unchanged.
63
184
 
64
185
  #### Verification evidence
65
186
 
66
- - CI passes on PRs #77 and #78.
187
+ - CI passes on PR #77.
67
188
 
68
189
  ---
69
190
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.6.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.6.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
+ ...