carson 1.0.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.
@@ -0,0 +1,177 @@
1
+ require "fileutils"
2
+ require "open3"
3
+ require "tmpdir"
4
+
5
+ module Carson
6
+ class Runtime
7
+ module Lint
8
+ # Prepares canonical lint policy files under ~/AI/CODING from an explicit source.
9
+ def lint_setup!( source:, ref: "main", force: false )
10
+ print_header "Lint Setup"
11
+ source_text = source.to_s.strip
12
+ if source_text.empty?
13
+ puts_line "ERROR: lint setup requires --source <path-or-git-url>."
14
+ return EXIT_ERROR
15
+ end
16
+
17
+ ref_text = ref.to_s.strip
18
+ ref_text = "main" if ref_text.empty?
19
+ source_dir, cleanup = lint_setup_source_directory( source: source_text, ref: ref_text )
20
+ begin
21
+ source_coding_dir = File.join( source_dir, "CODING" )
22
+ unless Dir.exist?( source_coding_dir )
23
+ puts_line "ERROR: source CODING directory not found at #{source_coding_dir}."
24
+ return EXIT_ERROR
25
+ end
26
+ target_coding_dir = ai_coding_dir
27
+ copy_result = copy_lint_coding_tree(
28
+ source_coding_dir: source_coding_dir,
29
+ target_coding_dir: target_coding_dir,
30
+ force: force
31
+ )
32
+ puts_line "lint_setup_source: #{source_text}"
33
+ puts_line "lint_setup_ref: #{ref_text}" if lint_source_git_url?( source: source_text )
34
+ puts_line "lint_setup_target: #{target_coding_dir}"
35
+ puts_line "lint_setup_created: #{copy_result.fetch( :created )}"
36
+ puts_line "lint_setup_updated: #{copy_result.fetch( :updated )}"
37
+ puts_line "lint_setup_skipped: #{copy_result.fetch( :skipped )}"
38
+
39
+ missing_policy = missing_lint_policy_files
40
+ if missing_policy.empty?
41
+ puts_line "OK: lint policy setup is complete."
42
+ return EXIT_OK
43
+ end
44
+
45
+ missing_policy.each do |entry|
46
+ puts_line "missing_lint_policy_file: language=#{entry.fetch( :language )} path=#{entry.fetch( :path )}"
47
+ end
48
+ puts_line "ACTION: update source CODING policy files, rerun carson lint setup, then rerun carson audit."
49
+ EXIT_ERROR
50
+ ensure
51
+ cleanup&.call
52
+ end
53
+ rescue StandardError => e
54
+ puts_line "ERROR: lint setup failed (#{e.message})"
55
+ EXIT_ERROR
56
+ end
57
+
58
+ private
59
+ def lint_setup_source_directory( source:, ref: )
60
+ if lint_source_git_url?( source: source )
61
+ return lint_setup_clone_source( source: source, ref: ref )
62
+ end
63
+
64
+ expanded_source = File.expand_path( source )
65
+ raise "source path does not exist: #{expanded_source}" unless Dir.exist?( expanded_source )
66
+ [ expanded_source, nil ]
67
+ end
68
+
69
+ def lint_source_git_url?( source: )
70
+ text = source.to_s.strip
71
+ text.start_with?( "https://", "http://", "ssh://", "git@", "file://" )
72
+ end
73
+
74
+ def lint_setup_clone_source( source:, ref: )
75
+ cache_root = cache_workspace_root
76
+ FileUtils.mkdir_p( cache_root )
77
+ work_dir = Dir.mktmpdir( "carson-lint-setup-", cache_root )
78
+ checkout_dir = File.join( work_dir, "source" )
79
+ clone_source = authenticated_lint_source( source: source )
80
+ stdout_text, stderr_text, status = Open3.capture3(
81
+ "git", "clone", "--depth", "1", "--branch", ref, clone_source, checkout_dir
82
+ )
83
+ unless status.success?
84
+ error_text = [ stderr_text.to_s.strip, stdout_text.to_s.strip ].reject( &:empty? ).join( " | " )
85
+ error_text = "git clone failed" if error_text.empty?
86
+ raise "unable to clone lint source #{safe_lint_source( source: source )} (#{error_text})"
87
+ end
88
+ [ checkout_dir, -> { FileUtils.rm_rf( work_dir ) } ]
89
+ end
90
+
91
+ def authenticated_lint_source( source: )
92
+ token = ENV.fetch( "CARSON_READ_TOKEN", "" ).to_s.strip
93
+ return source if token.empty?
94
+
95
+ return source unless source.start_with?( "https://github.com/", "http://github.com/", "git@github.com:" )
96
+
97
+ if source.start_with?( "git@github.com:" )
98
+ path = source.sub( "git@github.com:", "" )
99
+ return "https://x-access-token:#{token}@github.com/#{path}"
100
+ end
101
+
102
+ source.sub( %r{\Ahttps?://github\.com/}, "https://x-access-token:#{token}@github.com/" )
103
+ end
104
+
105
+ def safe_lint_source( source: )
106
+ source.to_s.gsub( %r{https://[^@]+@}, "https://***@" )
107
+ end
108
+
109
+ def cache_workspace_root
110
+ home = ENV.fetch( "HOME", "" ).to_s.strip
111
+ if home.start_with?( "/" )
112
+ path = File.join( home, ".cache", "carson" )
113
+ FileUtils.mkdir_p( path )
114
+ return path
115
+ end
116
+ "/tmp/carson"
117
+ rescue StandardError
118
+ "/tmp/carson"
119
+ end
120
+
121
+ def ai_coding_dir
122
+ home = ENV.fetch( "HOME", "" ).to_s.strip
123
+ raise "HOME must be an absolute path for lint setup" unless home.start_with?( "/" )
124
+
125
+ File.join( home, "AI", "CODING" )
126
+ end
127
+
128
+ def copy_lint_coding_tree( source_coding_dir:, target_coding_dir:, force: )
129
+ FileUtils.mkdir_p( target_coding_dir )
130
+ created = 0
131
+ updated = 0
132
+ skipped = 0
133
+ Dir.glob( "**/*", File::FNM_DOTMATCH, base: source_coding_dir ).sort.each do |relative|
134
+ next if [ ".", ".." ].include?( relative )
135
+ source_path = File.join( source_coding_dir, relative )
136
+ target_path = File.join( target_coding_dir, relative )
137
+ if File.directory?( source_path )
138
+ FileUtils.mkdir_p( target_path )
139
+ next
140
+ end
141
+ next unless File.file?( source_path )
142
+
143
+ if File.exist?( target_path ) && !force
144
+ skipped += 1
145
+ next
146
+ end
147
+ target_exists = File.exist?( target_path )
148
+ FileUtils.mkdir_p( File.dirname( target_path ) )
149
+ FileUtils.cp( source_path, target_path )
150
+ FileUtils.chmod( File.stat( source_path ).mode & 0o777, target_path )
151
+ if target_exists
152
+ updated += 1
153
+ else
154
+ created += 1
155
+ end
156
+ end
157
+ {
158
+ created: created,
159
+ updated: updated,
160
+ skipped: skipped
161
+ }
162
+ end
163
+
164
+ def missing_lint_policy_files
165
+ config.lint_languages.each_with_object( [] ) do |( language, entry ), missing|
166
+ next unless entry.fetch( :enabled )
167
+
168
+ entry.fetch( :config_files ).each do |path|
169
+ missing << { language: language, path: path } unless File.file?( path )
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ include Lint
176
+ end
177
+ end