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 +4 -4
- data/.github/workflows/carson_policy.yml +0 -7
- data/API.md +2 -1
- data/MANUAL.md +12 -1
- data/RELEASE.md +61 -0
- data/VERSION +1 -1
- data/carson.gemspec +12 -0
- data/lib/carson/cli.rb +3 -1
- data/lib/carson/config.rb +3 -2
- data/lib/carson/runtime/audit.rb +3 -11
- data/lib/carson/runtime/local.rb +17 -17
- data/lib/carson/runtime/setup.rb +295 -0
- data/lib/carson/runtime.rb +4 -2
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c3f3bf2f0ea58a2d460c36fb57b6313d5dab97c08b76ec94ae1c1e9b685b24f
|
|
4
|
+
data.tar.gz: 48562197450233540c28a6fa912c38eaa636910ed3bab57e32e95d7b0b66c10a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
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.
|
|
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
|
-
|
|
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" => "
|
|
32
|
+
"remote" => "origin",
|
|
32
33
|
"main_branch" => "main",
|
|
33
34
|
"protected_branches" => [ "main", "master" ]
|
|
34
35
|
},
|
data/lib/carson/runtime/audit.rb
CHANGED
|
@@ -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
|
-
|
|
209
|
-
remote_name
|
|
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
|
|
data/lib/carson/runtime/local.rb
CHANGED
|
@@ -161,7 +161,7 @@ module Carson
|
|
|
161
161
|
inspect!
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
# One-command onboarding for new repositories:
|
|
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
|
-
|
|
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
|
-
#
|
|
665
|
-
|
|
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
|
-
|
|
670
|
-
|
|
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 "
|
|
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
|
data/lib/carson/runtime.rb
CHANGED
|
@@ -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.
|
|
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
|
+
...
|