carson 2.7.0 → 2.8.1
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 +103 -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 +87 -40
- data/lib/carson/runtime/setup.rb +295 -0
- data/lib/carson/runtime.rb +22 -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: 76c0a5589cad84c4aaba1fee42bd667a21a55cea4840092b2984ae9eaeb84f6c
|
|
4
|
+
data.tar.gz: 656462b041f6764fd38780b500945ca0389b8235d741805367613051a27bd9da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 14cc66d4582c9ddfb9c5e87ec8e550d1f97cbcf6701a499e730dea4c06b5598ccdc6e8f61057a921ddba2d6ac9799fed4b3191a1a6dc1e440c4bc7249eec06bd
|
|
7
|
+
data.tar.gz: ce5e6f40342c08378830563ae0188cf61964d1f4650cc0be47a8cf52cb72f02b4f6413fa2b8d15a3dc70e3714628897446ab852ab7b276ac68a98cff0e5d1d54
|
|
@@ -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,109 @@ 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.1 — Onboard UX and Install Cleanup
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **Concise onboard output.** `carson onboard` now prints a clean 8-line summary instead of verbose internal state (hook paths, template statuses, config lines). Tells users what happened, what needs attention, and what to do next.
|
|
13
|
+
- **Graceful handling of fresh repos.** Onboard no longer fails with a fatal error on repositories with no commits yet.
|
|
14
|
+
- **Suppressed RubyGems PATH warning.** The misleading `WARNING: You don't have ... in your PATH, gem executables will not run` message from `gem install --user-install` is now suppressed during installation. Carson symlinks the executable to `~/.carson/bin`, making the gem bin directory irrelevant.
|
|
15
|
+
|
|
16
|
+
### What users must do now
|
|
17
|
+
|
|
18
|
+
1. Upgrade Carson to `2.8.1`.
|
|
19
|
+
|
|
20
|
+
### Breaking or removed behaviour
|
|
21
|
+
|
|
22
|
+
- None.
|
|
23
|
+
|
|
24
|
+
### Upgrade steps
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd ~/Dev/carson
|
|
28
|
+
git pull
|
|
29
|
+
bash install.sh
|
|
30
|
+
carson version
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Engineering Appendix
|
|
34
|
+
|
|
35
|
+
#### Modified components
|
|
36
|
+
|
|
37
|
+
- `lib/carson/runtime.rb` — added `concise?` flag and `with_captured_output` helper for suppressing sub-command detail during onboard.
|
|
38
|
+
- `lib/carson/runtime/local.rb` — rewrote `onboard!` to use concise orchestration (`onboard_apply!`), added `onboard_report_remote!` and `onboard_run_audit!` helpers, added `unless concise?` guards to `prepare!` and `template_apply!`.
|
|
39
|
+
- `install.sh` — capture `gem install` stderr and filter out RubyGems PATH warning.
|
|
40
|
+
- `script/install_global_carson.sh` — same PATH warning suppression.
|
|
41
|
+
- `test/runtime_govern_test.rb` — updated onboard output assertions.
|
|
42
|
+
|
|
43
|
+
#### Public interface and config changes
|
|
44
|
+
|
|
45
|
+
- No new CLI commands or config keys.
|
|
46
|
+
- Exit status contract unchanged.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 2.8.0 — Interactive Setup and Remote Detection
|
|
51
|
+
|
|
52
|
+
### What changed
|
|
53
|
+
|
|
54
|
+
- **`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.
|
|
55
|
+
- **Auto-triggered on first onboard.** `carson onboard` now launches the setup quiz when no `~/.carson/config.json` exists. Existing users are not affected.
|
|
56
|
+
- **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.
|
|
57
|
+
- **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.
|
|
58
|
+
- **Post-install message.** `gem install carson` now displays a getting-started guide pointing to `carson onboard`.
|
|
59
|
+
- **CI lint fallback simplified.** `lint_target_files_for_pull_request` uses `config.git_remote` directly with a minimal fallback to `origin`.
|
|
60
|
+
|
|
61
|
+
### What users must do now
|
|
62
|
+
|
|
63
|
+
1. Upgrade Carson to `2.8.0`.
|
|
64
|
+
2. If you relied on the `github` remote default, either rename your remote to `origin` or run `carson setup` to configure `git.remote`.
|
|
65
|
+
|
|
66
|
+
### Breaking or removed behaviour
|
|
67
|
+
|
|
68
|
+
- `git.remote` default changed from `github` to `origin`.
|
|
69
|
+
- `carson onboard` no longer renames `origin` to `github`.
|
|
70
|
+
- The `align_remote_name_for_carson!` method has been removed.
|
|
71
|
+
|
|
72
|
+
### Upgrade steps
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
cd ~/Dev/carson
|
|
76
|
+
git pull
|
|
77
|
+
bash install.sh
|
|
78
|
+
carson version
|
|
79
|
+
carson setup
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Engineering Appendix
|
|
83
|
+
|
|
84
|
+
#### New files
|
|
85
|
+
|
|
86
|
+
- `lib/carson/runtime/setup.rb` — interactive quiz, remote/branch detection, config persistence.
|
|
87
|
+
- `test/runtime_setup_test.rb` — quiz, detection, and config merge tests.
|
|
88
|
+
|
|
89
|
+
#### Modified components
|
|
90
|
+
|
|
91
|
+
- `lib/carson/config.rb` — default `git.remote` changed from `"github"` to `"origin"`, added `attr_accessor :git_remote`.
|
|
92
|
+
- `lib/carson/runtime.rb` — added `in_stream:` parameter and `@in` attribute.
|
|
93
|
+
- `lib/carson/cli.rb` — added `"setup"` command dispatch, updated banner.
|
|
94
|
+
- `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`.
|
|
95
|
+
- `lib/carson/runtime/audit.rb` — simplified `lint_target_files_for_pull_request` CI fallback.
|
|
96
|
+
- `test/runtime_govern_test.rb` — removed `origin` → `github` rename.
|
|
97
|
+
- `test/runtime_audit_lint_test.rb` — updated remote name from `github` to `origin`.
|
|
98
|
+
- `carson.gemspec` — added `spec.post_install_message`.
|
|
99
|
+
- `MANUAL.md` — documented `carson setup`, updated remote default.
|
|
100
|
+
- `API.md` — added `setup` command entry.
|
|
101
|
+
|
|
102
|
+
#### Public interface and config changes
|
|
103
|
+
|
|
104
|
+
- Added CLI command: `carson setup`.
|
|
105
|
+
- Default `git.remote` changed from `"github"` to `"origin"`.
|
|
106
|
+
- Runtime constructor accepts `in_stream:` keyword argument.
|
|
107
|
+
- Exit status contract unchanged.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
8
111
|
## 2.7.0 — Documentation and Test Fixes
|
|
9
112
|
|
|
10
113
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.8.1
|
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
|
@@ -153,40 +153,43 @@ module Carson
|
|
|
153
153
|
target_path = File.join( hooks_dir, hook_name )
|
|
154
154
|
FileUtils.cp( source_path, target_path )
|
|
155
155
|
FileUtils.chmod( 0o755, target_path )
|
|
156
|
-
puts_line "hook_written: #{relative_path( target_path )}"
|
|
156
|
+
puts_line "hook_written: #{relative_path( target_path )}" unless concise?
|
|
157
157
|
end
|
|
158
158
|
git_system!( "config", "core.hooksPath", hooks_dir )
|
|
159
159
|
File.write( File.join( hooks_dir, "workflow_style" ), config.workflow_style )
|
|
160
|
-
puts_line "configured_hooks_path: #{hooks_dir}"
|
|
160
|
+
puts_line "configured_hooks_path: #{hooks_dir}" unless concise?
|
|
161
|
+
return EXIT_OK if concise?
|
|
162
|
+
|
|
161
163
|
inspect!
|
|
162
164
|
end
|
|
163
165
|
|
|
164
|
-
# One-command onboarding for new repositories:
|
|
165
|
-
# apply templates, and
|
|
166
|
+
# One-command onboarding for new repositories: detect remote, install hooks,
|
|
167
|
+
# apply templates, and run initial audit.
|
|
166
168
|
def onboard!
|
|
167
169
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
168
170
|
return fingerprint_status unless fingerprint_status.nil?
|
|
169
171
|
|
|
170
|
-
print_header "Onboard"
|
|
171
172
|
unless inside_git_work_tree?
|
|
172
173
|
puts_line "ERROR: #{repo_root} is not a git repository."
|
|
173
174
|
return EXIT_ERROR
|
|
174
175
|
end
|
|
175
|
-
align_remote_name_for_carson!
|
|
176
|
-
hook_status = prepare!
|
|
177
|
-
return hook_status unless hook_status == EXIT_OK
|
|
178
176
|
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
repo_name = File.basename( repo_root )
|
|
178
|
+
puts_line ""
|
|
179
|
+
puts_line "Onboarding #{repo_name}..."
|
|
181
180
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
181
|
+
unless global_config_exists?
|
|
182
|
+
if self.in.respond_to?( :tty? ) && self.in.tty?
|
|
183
|
+
setup_status = setup!
|
|
184
|
+
return setup_status unless setup_status == EXIT_OK
|
|
185
|
+
else
|
|
186
|
+
silent_setup!
|
|
187
|
+
end
|
|
187
188
|
end
|
|
188
|
-
|
|
189
|
-
|
|
189
|
+
|
|
190
|
+
onboard_apply!
|
|
191
|
+
ensure
|
|
192
|
+
@concise = false
|
|
190
193
|
end
|
|
191
194
|
|
|
192
195
|
# Re-applies hooks, templates, and audit after upgrading Carson.
|
|
@@ -277,29 +280,29 @@ module Carson
|
|
|
277
280
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
278
281
|
return fingerprint_status unless fingerprint_status.nil?
|
|
279
282
|
|
|
280
|
-
print_header "Template Sync Apply"
|
|
283
|
+
print_header "Template Sync Apply" unless concise?
|
|
281
284
|
results = template_results
|
|
282
285
|
applied = 0
|
|
283
286
|
results.each do |entry|
|
|
284
287
|
if entry.fetch( :status ) == "error"
|
|
285
|
-
puts_line "template_file: #{entry.fetch( :file )} status=error reason=#{entry.fetch( :reason )}"
|
|
288
|
+
puts_line "template_file: #{entry.fetch( :file )} status=error reason=#{entry.fetch( :reason )}" unless concise?
|
|
286
289
|
next
|
|
287
290
|
end
|
|
288
291
|
|
|
289
292
|
file_path = File.join( repo_root, entry.fetch( :file ) )
|
|
290
293
|
if entry.fetch( :status ) == "ok"
|
|
291
|
-
puts_line "template_file: #{entry.fetch( :file )} status=ok reason=in_sync"
|
|
294
|
+
puts_line "template_file: #{entry.fetch( :file )} status=ok reason=in_sync" unless concise?
|
|
292
295
|
next
|
|
293
296
|
end
|
|
294
297
|
|
|
295
298
|
FileUtils.mkdir_p( File.dirname( file_path ) )
|
|
296
299
|
File.write( file_path, entry.fetch( :applied_content ) )
|
|
297
|
-
puts_line "template_file: #{entry.fetch( :file )} status=updated reason=#{entry.fetch( :reason )}"
|
|
300
|
+
puts_line "template_file: #{entry.fetch( :file )} status=updated reason=#{entry.fetch( :reason )}" unless concise?
|
|
298
301
|
applied += 1
|
|
299
302
|
end
|
|
300
303
|
|
|
301
304
|
error_count = results.count { |entry| entry.fetch( :status ) == "error" }
|
|
302
|
-
puts_line "template_apply_summary: updated=#{applied} error=#{error_count}"
|
|
305
|
+
puts_line "template_apply_summary: updated=#{applied} error=#{error_count}" unless concise?
|
|
303
306
|
error_count.positive? ? EXIT_ERROR : EXIT_OK
|
|
304
307
|
end
|
|
305
308
|
|
|
@@ -661,31 +664,75 @@ module Carson
|
|
|
661
664
|
end
|
|
662
665
|
end
|
|
663
666
|
|
|
664
|
-
#
|
|
665
|
-
|
|
666
|
-
def align_remote_name_for_carson!
|
|
667
|
+
# Verifies configured remote exists and logs status without mutating remotes.
|
|
668
|
+
def report_detected_remote!
|
|
667
669
|
if git_remote_exists?( remote_name: config.git_remote )
|
|
668
670
|
puts_line "remote_ok: #{config.git_remote}"
|
|
669
|
-
|
|
671
|
+
else
|
|
672
|
+
puts_line "WARN: remote '#{config.git_remote}' not found; run carson setup to configure."
|
|
670
673
|
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
|
|
675
|
-
end
|
|
676
|
-
puts_line "WARN: no #{config.git_remote} or origin remote configured; continue with local baseline only."
|
|
677
674
|
end
|
|
678
675
|
|
|
676
|
+
# Concise onboard orchestration: hooks, templates, remote, audit, guidance.
|
|
677
|
+
def onboard_apply!
|
|
678
|
+
@concise = true
|
|
679
|
+
|
|
680
|
+
hook_status = prepare!
|
|
681
|
+
return hook_status unless hook_status == EXIT_OK
|
|
682
|
+
puts_line "Hooks installed (#{config.required_hooks.count} hooks)."
|
|
683
|
+
|
|
684
|
+
template_drift_count = template_results.count { |entry| entry.fetch( :status ) != "ok" }
|
|
685
|
+
template_status = template_apply!
|
|
686
|
+
return template_status unless template_status == EXIT_OK
|
|
687
|
+
if template_drift_count.positive?
|
|
688
|
+
puts_line "Templates synced (#{template_drift_count} file#{plural_suffix( count: template_drift_count )} updated)."
|
|
689
|
+
else
|
|
690
|
+
puts_line "Templates in sync."
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
onboard_report_remote!
|
|
694
|
+
audit_status = onboard_run_audit!
|
|
679
695
|
|
|
680
|
-
def print_onboarding_guidance
|
|
681
|
-
puts_line ""
|
|
682
|
-
puts_line "Carson is ready. Current workflow: #{config.workflow_style}"
|
|
683
|
-
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
696
|
puts_line ""
|
|
688
|
-
puts_line "
|
|
697
|
+
puts_line "Carson is ready. Workflow: #{config.workflow_style}"
|
|
698
|
+
puts_line "Reconfigure anytime: carson setup"
|
|
699
|
+
audit_status
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Friendly remote status for onboard output.
|
|
703
|
+
def onboard_report_remote!
|
|
704
|
+
if git_remote_exists?( remote_name: config.git_remote )
|
|
705
|
+
puts_line "Remote: #{config.git_remote} (connected)."
|
|
706
|
+
else
|
|
707
|
+
puts_line "Remote not configured yet — carson setup will walk you through it."
|
|
708
|
+
end
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Runs audit with captured output; reports summary instead of full detail.
|
|
712
|
+
def onboard_run_audit!
|
|
713
|
+
audit_error = nil
|
|
714
|
+
audit_status = with_captured_output { audit! }
|
|
715
|
+
rescue StandardError => e
|
|
716
|
+
audit_error = e
|
|
717
|
+
audit_status = EXIT_OK
|
|
718
|
+
ensure
|
|
719
|
+
return onboard_print_audit_result( status: audit_status, error: audit_error )
|
|
720
|
+
end
|
|
721
|
+
|
|
722
|
+
def onboard_print_audit_result( status:, error: )
|
|
723
|
+
if error
|
|
724
|
+
if error.message.to_s.match?( /HEAD|rev-parse/ )
|
|
725
|
+
puts_line "No commits yet — run carson audit after your first commit."
|
|
726
|
+
else
|
|
727
|
+
puts_line "Audit skipped — run carson audit for details."
|
|
728
|
+
end
|
|
729
|
+
return EXIT_OK
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
if status == EXIT_BLOCK
|
|
733
|
+
puts_line "Some checks need attention — run carson audit for details."
|
|
734
|
+
end
|
|
735
|
+
status
|
|
689
736
|
end
|
|
690
737
|
|
|
691
738
|
# Uses `git remote get-url` as existence check to avoid parsing remote lists.
|
|
@@ -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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require "json"
|
|
6
6
|
require "open3"
|
|
7
|
+
require "stringio"
|
|
7
8
|
require "time"
|
|
8
9
|
|
|
9
10
|
module Carson
|
|
@@ -22,11 +23,13 @@ module Carson
|
|
|
22
23
|
DISPOSITION_TOKENS = %w[accepted rejected deferred].freeze
|
|
23
24
|
|
|
24
25
|
# Runtime wiring for repository context, tool paths, and output streams.
|
|
25
|
-
def initialize( repo_root:, tool_root:, out:, err: )
|
|
26
|
+
def initialize( repo_root:, tool_root:, out:, err:, in_stream: $stdin )
|
|
26
27
|
@repo_root = repo_root
|
|
27
28
|
@tool_root = tool_root
|
|
28
29
|
@out = out
|
|
29
30
|
@err = err
|
|
31
|
+
@in = in_stream
|
|
32
|
+
@concise = false
|
|
30
33
|
@config = Config.load( repo_root: repo_root )
|
|
31
34
|
@git_adapter = Adapters::Git.new( repo_root: repo_root )
|
|
32
35
|
@github_adapter = Adapters::GitHub.new( repo_root: repo_root )
|
|
@@ -34,7 +37,23 @@ module Carson
|
|
|
34
37
|
|
|
35
38
|
private
|
|
36
39
|
|
|
37
|
-
attr_reader :repo_root, :tool_root, :out, :err, :config, :git_adapter, :github_adapter
|
|
40
|
+
attr_reader :repo_root, :tool_root, :out, :err, :in, :config, :git_adapter, :github_adapter
|
|
41
|
+
|
|
42
|
+
# Returns true when output should be minimal (used by onboard/refresh).
|
|
43
|
+
def concise?
|
|
44
|
+
@concise
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Runs a block with all output captured (suppressed from the user).
|
|
48
|
+
# Returns the block's return value; output is silently discarded.
|
|
49
|
+
def with_captured_output
|
|
50
|
+
saved_out, saved_err = @out, @err
|
|
51
|
+
@out = StringIO.new
|
|
52
|
+
@err = StringIO.new
|
|
53
|
+
yield
|
|
54
|
+
ensure
|
|
55
|
+
@out, @err = saved_out, saved_err
|
|
56
|
+
end
|
|
38
57
|
|
|
39
58
|
# Current local branch name.
|
|
40
59
|
def current_branch
|
|
@@ -187,3 +206,4 @@ require_relative "runtime/lint"
|
|
|
187
206
|
require_relative "runtime/audit"
|
|
188
207
|
require_relative "runtime/review"
|
|
189
208
|
require_relative "runtime/govern"
|
|
209
|
+
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.1
|
|
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
|
+
...
|