sxn 0.2.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.
Files changed (156) hide show
  1. checksums.yaml +7 -0
  2. data/.gem_rbs_collection/addressable/2.8/.rbs_meta.yaml +9 -0
  3. data/.gem_rbs_collection/addressable/2.8/addressable.rbs +62 -0
  4. data/.gem_rbs_collection/async/2.12/.rbs_meta.yaml +9 -0
  5. data/.gem_rbs_collection/async/2.12/async.rbs +119 -0
  6. data/.gem_rbs_collection/async/2.12/kernel.rbs +5 -0
  7. data/.gem_rbs_collection/async/2.12/manifest.yaml +7 -0
  8. data/.gem_rbs_collection/bcrypt/3.1/.rbs_meta.yaml +9 -0
  9. data/.gem_rbs_collection/bcrypt/3.1/bcrypt.rbs +47 -0
  10. data/.gem_rbs_collection/bcrypt/3.1/manifest.yaml +2 -0
  11. data/.gem_rbs_collection/bigdecimal/3.1/.rbs_meta.yaml +9 -0
  12. data/.gem_rbs_collection/bigdecimal/3.1/bigdecimal-math.rbs +119 -0
  13. data/.gem_rbs_collection/bigdecimal/3.1/bigdecimal.rbs +1630 -0
  14. data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
  15. data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
  16. data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +26 -0
  17. data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
  18. data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +65 -0
  19. data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
  20. data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
  21. data/.gem_rbs_collection/diff-lcs/1.5/.rbs_meta.yaml +9 -0
  22. data/.gem_rbs_collection/diff-lcs/1.5/diff-lcs.rbs +11 -0
  23. data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
  24. data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
  25. data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
  26. data/.gem_rbs_collection/mini_mime/0.1/.rbs_meta.yaml +9 -0
  27. data/.gem_rbs_collection/mini_mime/0.1/mini_mime.rbs +14 -0
  28. data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
  29. data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
  30. data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
  31. data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
  32. data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
  33. data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
  34. data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +822 -0
  35. data/.gem_rbs_collection/sqlite3/2.0/.rbs_meta.yaml +9 -0
  36. data/.gem_rbs_collection/sqlite3/2.0/database.rbs +20 -0
  37. data/.gem_rbs_collection/sqlite3/2.0/pragmas.rbs +5 -0
  38. data/.rspec +4 -0
  39. data/.rubocop.yml +121 -0
  40. data/.simplecov +51 -0
  41. data/CHANGELOG.md +49 -0
  42. data/Gemfile +24 -0
  43. data/Gemfile.lock +329 -0
  44. data/LICENSE.txt +21 -0
  45. data/README.md +225 -0
  46. data/Rakefile +54 -0
  47. data/Steepfile +50 -0
  48. data/bin/sxn +6 -0
  49. data/lib/sxn/CLI.rb +275 -0
  50. data/lib/sxn/commands/init.rb +137 -0
  51. data/lib/sxn/commands/projects.rb +350 -0
  52. data/lib/sxn/commands/rules.rb +435 -0
  53. data/lib/sxn/commands/sessions.rb +300 -0
  54. data/lib/sxn/commands/worktrees.rb +416 -0
  55. data/lib/sxn/commands.rb +13 -0
  56. data/lib/sxn/config/config_cache.rb +295 -0
  57. data/lib/sxn/config/config_discovery.rb +242 -0
  58. data/lib/sxn/config/config_validator.rb +562 -0
  59. data/lib/sxn/config.rb +259 -0
  60. data/lib/sxn/core/config_manager.rb +290 -0
  61. data/lib/sxn/core/project_manager.rb +307 -0
  62. data/lib/sxn/core/rules_manager.rb +306 -0
  63. data/lib/sxn/core/session_manager.rb +336 -0
  64. data/lib/sxn/core/worktree_manager.rb +281 -0
  65. data/lib/sxn/core.rb +13 -0
  66. data/lib/sxn/database/errors.rb +29 -0
  67. data/lib/sxn/database/session_database.rb +691 -0
  68. data/lib/sxn/database.rb +24 -0
  69. data/lib/sxn/errors.rb +76 -0
  70. data/lib/sxn/rules/base_rule.rb +367 -0
  71. data/lib/sxn/rules/copy_files_rule.rb +346 -0
  72. data/lib/sxn/rules/errors.rb +28 -0
  73. data/lib/sxn/rules/project_detector.rb +871 -0
  74. data/lib/sxn/rules/rules_engine.rb +485 -0
  75. data/lib/sxn/rules/setup_commands_rule.rb +307 -0
  76. data/lib/sxn/rules/template_rule.rb +262 -0
  77. data/lib/sxn/rules.rb +148 -0
  78. data/lib/sxn/runtime_validations.rb +96 -0
  79. data/lib/sxn/security/secure_command_executor.rb +364 -0
  80. data/lib/sxn/security/secure_file_copier.rb +478 -0
  81. data/lib/sxn/security/secure_path_validator.rb +258 -0
  82. data/lib/sxn/security.rb +15 -0
  83. data/lib/sxn/templates/common/gitignore.liquid +99 -0
  84. data/lib/sxn/templates/common/session-info.md.liquid +58 -0
  85. data/lib/sxn/templates/errors.rb +36 -0
  86. data/lib/sxn/templates/javascript/README.md.liquid +59 -0
  87. data/lib/sxn/templates/javascript/session-info.md.liquid +206 -0
  88. data/lib/sxn/templates/rails/CLAUDE.md.liquid +78 -0
  89. data/lib/sxn/templates/rails/database.yml.liquid +31 -0
  90. data/lib/sxn/templates/rails/session-info.md.liquid +144 -0
  91. data/lib/sxn/templates/template_engine.rb +346 -0
  92. data/lib/sxn/templates/template_processor.rb +279 -0
  93. data/lib/sxn/templates/template_security.rb +410 -0
  94. data/lib/sxn/templates/template_variables.rb +713 -0
  95. data/lib/sxn/templates.rb +28 -0
  96. data/lib/sxn/ui/output.rb +103 -0
  97. data/lib/sxn/ui/progress_bar.rb +91 -0
  98. data/lib/sxn/ui/prompt.rb +116 -0
  99. data/lib/sxn/ui/table.rb +183 -0
  100. data/lib/sxn/ui.rb +12 -0
  101. data/lib/sxn/version.rb +5 -0
  102. data/lib/sxn.rb +63 -0
  103. data/rbs_collection.lock.yaml +180 -0
  104. data/rbs_collection.yaml +39 -0
  105. data/scripts/test.sh +31 -0
  106. data/sig/external/liquid.rbs +116 -0
  107. data/sig/external/thor.rbs +99 -0
  108. data/sig/external/tty.rbs +71 -0
  109. data/sig/sxn/cli.rbs +46 -0
  110. data/sig/sxn/commands/init.rbs +38 -0
  111. data/sig/sxn/commands/projects.rbs +72 -0
  112. data/sig/sxn/commands/rules.rbs +95 -0
  113. data/sig/sxn/commands/sessions.rbs +62 -0
  114. data/sig/sxn/commands/worktrees.rbs +82 -0
  115. data/sig/sxn/commands.rbs +6 -0
  116. data/sig/sxn/config/config_cache.rbs +67 -0
  117. data/sig/sxn/config/config_discovery.rbs +64 -0
  118. data/sig/sxn/config/config_validator.rbs +64 -0
  119. data/sig/sxn/config.rbs +74 -0
  120. data/sig/sxn/core/config_manager.rbs +67 -0
  121. data/sig/sxn/core/project_manager.rbs +52 -0
  122. data/sig/sxn/core/rules_manager.rbs +54 -0
  123. data/sig/sxn/core/session_manager.rbs +59 -0
  124. data/sig/sxn/core/worktree_manager.rbs +50 -0
  125. data/sig/sxn/core.rbs +87 -0
  126. data/sig/sxn/database/errors.rbs +37 -0
  127. data/sig/sxn/database/session_database.rbs +151 -0
  128. data/sig/sxn/database.rbs +83 -0
  129. data/sig/sxn/errors.rbs +89 -0
  130. data/sig/sxn/rules/base_rule.rbs +137 -0
  131. data/sig/sxn/rules/copy_files_rule.rbs +65 -0
  132. data/sig/sxn/rules/errors.rbs +33 -0
  133. data/sig/sxn/rules/project_detector.rbs +115 -0
  134. data/sig/sxn/rules/rules_engine.rbs +118 -0
  135. data/sig/sxn/rules/setup_commands_rule.rbs +60 -0
  136. data/sig/sxn/rules/template_rule.rbs +44 -0
  137. data/sig/sxn/rules.rbs +287 -0
  138. data/sig/sxn/runtime_validations.rbs +16 -0
  139. data/sig/sxn/security/secure_command_executor.rbs +63 -0
  140. data/sig/sxn/security/secure_file_copier.rbs +79 -0
  141. data/sig/sxn/security/secure_path_validator.rbs +30 -0
  142. data/sig/sxn/security.rbs +128 -0
  143. data/sig/sxn/templates/errors.rbs +43 -0
  144. data/sig/sxn/templates/template_engine.rbs +50 -0
  145. data/sig/sxn/templates/template_processor.rbs +44 -0
  146. data/sig/sxn/templates/template_security.rbs +62 -0
  147. data/sig/sxn/templates/template_variables.rbs +103 -0
  148. data/sig/sxn/templates.rbs +104 -0
  149. data/sig/sxn/ui/output.rbs +50 -0
  150. data/sig/sxn/ui/progress_bar.rbs +39 -0
  151. data/sig/sxn/ui/prompt.rbs +38 -0
  152. data/sig/sxn/ui/table.rbs +43 -0
  153. data/sig/sxn/ui.rbs +63 -0
  154. data/sig/sxn/version.rbs +5 -0
  155. data/sig/sxn.rbs +29 -0
  156. metadata +635 -0
@@ -0,0 +1,871 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "yaml"
5
+ require "pathname"
6
+
7
+ module Sxn
8
+ module Rules
9
+ # ProjectDetector analyzes project directories to determine their type, language,
10
+ # package manager, and suggests appropriate default rules for project setup.
11
+ #
12
+ # @example Basic usage
13
+ # detector = ProjectDetector.new("/path/to/project")
14
+ # info = detector.detect_project_info
15
+ # puts "Project type: #{info[:type]}"
16
+ # puts "Package manager: #{info[:package_manager]}"
17
+ #
18
+ # rules = detector.suggest_default_rules
19
+ # puts "Suggested rules: #{rules.keys}"
20
+ #
21
+ class ProjectDetector
22
+ # Project type definitions with their detection criteria
23
+ PROJECT_TYPES = {
24
+ rails: {
25
+ files: %w[Gemfile config/application.rb],
26
+ patterns: {
27
+ gemfile_contains: ["rails"]
28
+ },
29
+ confidence: :high
30
+ },
31
+ ruby: {
32
+ files: %w[Gemfile *.gemspec],
33
+ patterns: {},
34
+ confidence: :medium
35
+ },
36
+ nextjs: {
37
+ files: %w[package.json next.config.js],
38
+ patterns: {
39
+ package_json_deps: ["next"]
40
+ },
41
+ confidence: :high
42
+ },
43
+ react: {
44
+ files: %w[package.json],
45
+ patterns: {
46
+ package_json_deps: ["react"]
47
+ },
48
+ confidence: :high
49
+ },
50
+ nodejs: {
51
+ files: %w[package.json],
52
+ patterns: {
53
+ package_json_deps: ["express", "fastify", "koa", "@types/node", "nodemon", "typescript"]
54
+ },
55
+ confidence: :medium_high
56
+ },
57
+ javascript: {
58
+ files: %w[package.json],
59
+ patterns: {},
60
+ confidence: :medium
61
+ },
62
+ typescript: {
63
+ files: %w[tsconfig.json *.ts],
64
+ patterns: {},
65
+ confidence: :high
66
+ },
67
+ python: {
68
+ files: %w[requirements.txt setup.py pyproject.toml Pipfile],
69
+ patterns: {},
70
+ confidence: :medium
71
+ },
72
+ django: {
73
+ files: %w[manage.py],
74
+ patterns: {
75
+ requirements_contains: ["django"]
76
+ },
77
+ confidence: :high
78
+ },
79
+ go: {
80
+ files: %w[go.mod go.sum *.go],
81
+ patterns: {},
82
+ confidence: :high
83
+ },
84
+ rust: {
85
+ files: %w[Cargo.toml Cargo.lock],
86
+ patterns: {},
87
+ confidence: :high
88
+ }
89
+ }.freeze
90
+
91
+ # Package manager detection patterns
92
+ PACKAGE_MANAGERS = {
93
+ bundler: {
94
+ files: %w[Gemfile Gemfile.lock],
95
+ command: "bundle"
96
+ },
97
+ npm: {
98
+ files: %w[package-lock.json],
99
+ command: "npm"
100
+ },
101
+ yarn: {
102
+ files: %w[yarn.lock],
103
+ command: "yarn"
104
+ },
105
+ pnpm: {
106
+ files: %w[pnpm-lock.yaml],
107
+ command: "pnpm"
108
+ },
109
+ pip: {
110
+ files: %w[requirements.txt],
111
+ command: "pip"
112
+ },
113
+ pipenv: {
114
+ files: %w[Pipfile Pipfile.lock],
115
+ command: "pipenv"
116
+ },
117
+ poetry: {
118
+ files: %w[pyproject.toml poetry.lock],
119
+ command: "poetry"
120
+ },
121
+ cargo: {
122
+ files: %w[Cargo.toml Cargo.lock],
123
+ command: "cargo"
124
+ },
125
+ go_mod: {
126
+ files: %w[go.mod go.sum],
127
+ command: "go"
128
+ }
129
+ }.freeze
130
+
131
+ attr_reader :project_path
132
+
133
+ # Initialize the project detector
134
+ #
135
+ # @param project_path [String] Absolute path to the project directory
136
+ def initialize(project_path)
137
+ raise ArgumentError, "Project path cannot be nil or empty" if project_path.nil? || project_path.empty?
138
+
139
+ @project_path = File.realpath(project_path)
140
+ validate_project_path!
141
+ rescue Errno::ENOENT
142
+ raise ArgumentError, "Project path does not exist: #{project_path}"
143
+ end
144
+
145
+ # Detect comprehensive project information
146
+ #
147
+ # @return [Hash] Project information including type, language, package manager, etc.
148
+ def detect_project_info
149
+ {
150
+ type: detect_project_type,
151
+ language: detect_primary_language,
152
+ languages: detect_all_languages,
153
+ package_manager: detect_package_manager,
154
+ framework: detect_framework,
155
+ has_docker: has_docker?,
156
+ has_tests: has_tests?,
157
+ has_ci: has_ci_config?,
158
+ database: detect_database,
159
+ sensitive_files: detect_sensitive_files,
160
+ analysis_timestamp: Time.now.iso8601
161
+ }
162
+ end
163
+
164
+ # Detect project type for a given path (used by ConfigManager)
165
+ #
166
+ # @param path [String] Path to the project directory
167
+ # @return [Symbol] Detected project type (:rails, :nodejs, :python, etc.)
168
+ def detect_type(path)
169
+ old_path = @project_path
170
+ @project_path = File.realpath(path)
171
+ result = detect_project_type
172
+ @project_path = old_path
173
+ result
174
+ rescue Errno::ENOENT
175
+ :unknown
176
+ end
177
+
178
+ # Legacy method for compatibility with tests
179
+ # Detect the primary project type
180
+ #
181
+ # @return [Symbol] Detected project type (:rails, :nodejs, :python, etc.)
182
+ def detect_project_type
183
+ detected_types = []
184
+
185
+ PROJECT_TYPES.each do |type, criteria|
186
+ confidence = calculate_type_confidence(type, criteria)
187
+ detected_types << { type: type, confidence: confidence } if confidence.positive?
188
+ end
189
+
190
+ # Sort by confidence and return the highest
191
+ detected_types.min_by { |t| -t[:confidence] }&.fetch(:type) || :unknown
192
+ end
193
+
194
+ # Detect the package manager used by the project
195
+ #
196
+ # @return [Symbol] Detected package manager (:bundler, :npm, :yarn, etc.)
197
+ def detect_package_manager
198
+ PACKAGE_MANAGERS.each do |manager, criteria|
199
+ return manager if criteria[:files].any? { |file| file_exists_in_project?(file) }
200
+ end
201
+
202
+ # Fallback logic for common scenarios
203
+ if file_exists_in_project?("package.json")
204
+ return :npm # Default to npm for Node.js projects without specific lock files
205
+ end
206
+
207
+ if file_exists_in_project?("Gemfile")
208
+ return :bundler # Default to bundler for Ruby projects without lock files
209
+ end
210
+
211
+ :unknown
212
+ end
213
+
214
+ # Suggest default rules based on detected project characteristics
215
+ #
216
+ # @return [Hash] Suggested rules configuration
217
+ def suggest_default_rules
218
+ project_info = detect_project_info
219
+ rules = {}
220
+
221
+ # Add copy files rules based on project type
222
+ copy_files = suggest_copy_files_rules(project_info)
223
+ rules["copy_files"] = copy_files unless copy_files["config"]["files"] && copy_files["config"]["files"].empty?
224
+
225
+ # Add setup commands rules based on package manager
226
+ setup_commands = suggest_setup_commands_rules(project_info)
227
+ unless setup_commands["config"]["commands"] && setup_commands["config"]["commands"].empty?
228
+ rules["setup_commands"] =
229
+ setup_commands
230
+ end
231
+
232
+ # Add template rules for common project documentation
233
+ template_rules = suggest_template_rules(project_info)
234
+ unless template_rules["config"]["templates"] && template_rules["config"]["templates"].empty?
235
+ rules["templates"] =
236
+ template_rules
237
+ end
238
+
239
+ rules
240
+ end
241
+
242
+ # Get detailed analysis of the project structure
243
+ #
244
+ # @return [Hash] Detailed project analysis
245
+ def analyze_project_structure
246
+ {
247
+ files: analyze_important_files,
248
+ directories: analyze_directory_structure,
249
+ dependencies: analyze_dependencies,
250
+ configuration: analyze_configuration_files,
251
+ scripts: analyze_scripts,
252
+ documentation: analyze_documentation
253
+ }
254
+ end
255
+
256
+ private
257
+
258
+ # Validate that the project path exists and is a directory
259
+ def validate_project_path!
260
+ raise ArgumentError, "Project path is not a directory: #{@project_path}" unless File.directory?(@project_path)
261
+
262
+ return if File.readable?(@project_path)
263
+
264
+ raise ArgumentError, "Project path is not readable: #{@project_path}"
265
+ end
266
+
267
+ # Calculate confidence score for a project type
268
+ def calculate_type_confidence(type, criteria)
269
+ confidence = 0
270
+
271
+ # Check for required files
272
+ files_found = criteria[:files].count { |file| file_exists_in_project?(file) }
273
+ if files_found.positive?
274
+ confidence += files_found * 10
275
+ confidence += 20 if files_found == criteria[:files].length
276
+ end
277
+
278
+ # For high-confidence project types with specific patterns,
279
+ # require all files AND pattern matches to be valid
280
+ if criteria[:confidence] == :high && !criteria[:patterns].empty?
281
+ return 0 unless files_found == criteria[:files].length
282
+
283
+ # All patterns must match for high-confidence types
284
+ pattern_matches = 0
285
+ criteria[:patterns].each do |pattern_type, patterns|
286
+ case pattern_type
287
+ when :gemfile_contains
288
+ pattern_matches += 1 if patterns.any? { |pattern| gemfile_contains?(pattern) }
289
+ when :package_json_deps
290
+ pattern_matches += 1 if patterns.any? { |dep| package_json_has_dependency?(dep) }
291
+ when :requirements_contains
292
+ pattern_matches += 1 if patterns.any? { |pattern| requirements_contains?(pattern) }
293
+ end
294
+ end
295
+
296
+ return 0 unless pattern_matches == criteria[:patterns].length
297
+
298
+ confidence += pattern_matches * 30
299
+ else
300
+ # For other types, add confidence for pattern matches
301
+ criteria[:patterns].each do |pattern_type, patterns|
302
+ case pattern_type
303
+ when :gemfile_contains
304
+ confidence += 30 if patterns.any? { |pattern| gemfile_contains?(pattern) }
305
+ when :package_json_deps
306
+ confidence += 30 if patterns.any? { |dep| package_json_has_dependency?(dep) }
307
+ when :requirements_contains
308
+ confidence += 30 if patterns.any? { |pattern| requirements_contains?(pattern) }
309
+ end
310
+ end
311
+ end
312
+
313
+ # Special logic for Node.js vs JavaScript distinction
314
+ # Only apply when using actual PROJECT_TYPES criteria for nodejs
315
+ # Don't boost Node.js confidence if this looks like a TypeScript project
316
+ if type == :nodejs && file_exists_in_project?("package.json") &&
317
+ criteria == PROJECT_TYPES[:nodejs] &&
318
+ !(file_exists_in_project?("tsconfig.json") && file_exists_in_project?("*.ts")) &&
319
+ has_nodejs_characteristics?
320
+ # Only boost Node.js if it has typical Node.js characteristics
321
+ # Otherwise treat it as plain JavaScript
322
+ confidence += 50
323
+ end
324
+
325
+ # Apply confidence modifiers
326
+ case criteria[:confidence]
327
+ when :high
328
+ confidence *= 1.2
329
+ when :medium_high
330
+ confidence *= 1.1
331
+ when :low
332
+ confidence *= 0.8
333
+ end
334
+
335
+ confidence.to_i
336
+ end
337
+
338
+ # Calculate confidence score for a specific project type (test compatibility)
339
+ def calculate_confidence_score(type)
340
+ criteria = PROJECT_TYPES[type]
341
+ return 0 unless criteria
342
+
343
+ calculate_type_confidence(type, criteria)
344
+ end
345
+
346
+ # Check if a file exists in the project (supports glob patterns)
347
+ def file_exists_in_project?(file_pattern)
348
+ return false unless @project_path && File.directory?(@project_path)
349
+
350
+ if file_pattern.include?("*")
351
+ !Dir.glob(File.join(@project_path, file_pattern)).empty?
352
+ else
353
+ File.exist?(File.join(@project_path, file_pattern))
354
+ end
355
+ rescue Errno::EACCES, Errno::EIO, StandardError
356
+ # Handle permission errors and I/O errors gracefully
357
+ false
358
+ end
359
+
360
+ # Detect primary programming language
361
+ def detect_primary_language
362
+ return :unknown unless @project_path && File.directory?(@project_path)
363
+
364
+ language_files = {
365
+ ruby: %w[*.rb Gemfile Rakefile],
366
+ javascript: %w[*.js *.jsx package.json],
367
+ typescript: %w[*.ts *.tsx tsconfig.json],
368
+ python: %w[*.py requirements.txt setup.py],
369
+ go: %w[*.go go.mod],
370
+ rust: %w[*.rs Cargo.toml],
371
+ java: %w[*.java pom.xml build.gradle],
372
+ php: %w[*.php composer.json],
373
+ csharp: %w[*.cs *.csproj],
374
+ cpp: %w[*.cpp *.hpp *.cmake CMakeLists.txt]
375
+ }
376
+
377
+ language_scores = {}
378
+
379
+ language_files.each do |language, patterns|
380
+ score = patterns.sum do |pattern|
381
+ if pattern.include?("*")
382
+ Dir.glob(File.join(@project_path, "**", pattern)).length
383
+ else
384
+ file_exists_in_project?(pattern) ? 10 : 0
385
+ end
386
+ rescue Errno::EACCES, Errno::EIO, StandardError
387
+ 0
388
+ end
389
+ language_scores[language] = score
390
+ end
391
+
392
+ language_scores.max_by { |_, score| score }&.first || :unknown
393
+ rescue StandardError
394
+ :unknown
395
+ end
396
+
397
+ # Detect all languages present in the project
398
+ def detect_all_languages
399
+ return [] unless @project_path && File.directory?(@project_path)
400
+
401
+ language_files = {
402
+ ruby: %w[*.rb Gemfile Rakefile],
403
+ javascript: %w[*.js *.jsx package.json],
404
+ typescript: %w[*.ts *.tsx tsconfig.json],
405
+ python: %w[*.py requirements.txt setup.py],
406
+ go: %w[*.go go.mod],
407
+ rust: %w[*.rs Cargo.toml],
408
+ java: %w[*.java pom.xml build.gradle],
409
+ php: %w[*.php composer.json],
410
+ csharp: %w[*.cs *.csproj],
411
+ cpp: %w[*.cpp *.hpp *.cmake CMakeLists.txt]
412
+ }
413
+
414
+ detected_languages = []
415
+ language_files.each do |language, patterns|
416
+ score = patterns.sum do |pattern|
417
+ if pattern.include?("*")
418
+ Dir.glob(File.join(@project_path, "**", pattern)).length
419
+ else
420
+ file_exists_in_project?(pattern) ? 1 : 0
421
+ end
422
+ rescue Errno::EACCES, Errno::EIO, StandardError
423
+ 0
424
+ end
425
+ detected_languages << language if score.positive?
426
+ end
427
+
428
+ detected_languages
429
+ rescue StandardError
430
+ []
431
+ end
432
+
433
+ # Detect web framework
434
+ def detect_framework
435
+ return :rails if gemfile_contains?("rails")
436
+ return :django if requirements_contains?("django")
437
+ return :nextjs if package_json_has_dependency?("next")
438
+ return :react if package_json_has_dependency?("react")
439
+ return :vue if package_json_has_dependency?("vue")
440
+ return :express if package_json_has_dependency?("express")
441
+ return :fastapi if requirements_contains?("fastapi")
442
+ return :flask if requirements_contains?("flask")
443
+
444
+ :unknown
445
+ end
446
+
447
+ # Check if project has Docker configuration
448
+ def has_docker?
449
+ file_exists_in_project?("Dockerfile") ||
450
+ file_exists_in_project?("docker-compose.yml") ||
451
+ file_exists_in_project?("docker-compose.yaml")
452
+ end
453
+
454
+ # Check if project has test configuration
455
+ def has_tests?
456
+ test_patterns = %w[
457
+ spec test tests __tests__ *.test.* *.spec.*
458
+ pytest.ini tox.ini jest.config.* vitest.config.*
459
+ ]
460
+
461
+ test_patterns.any? { |pattern| file_exists_in_project?(pattern) }
462
+ end
463
+
464
+ # Check if project has CI configuration
465
+ def has_ci_config?
466
+ ci_files = %w[
467
+ .github/workflows .gitlab-ci.yml .circleci/config.yml
468
+ .travis.yml appveyor.yml .buildkite
469
+ ]
470
+
471
+ ci_files.any? { |file| file_exists_in_project?(file) }
472
+ end
473
+
474
+ # Detect database configuration
475
+ def detect_database
476
+ databases = []
477
+
478
+ # Check configuration files and environment files
479
+ databases << :postgresql if file_contains?("config/database.yml",
480
+ "postgresql") || env_contains?("DATABASE_URL",
481
+ "postgres") || file_contains?(".env",
482
+ "postgresql://")
483
+ databases << :mysql if file_contains?("config/database.yml",
484
+ "mysql") || env_contains?("DATABASE_URL",
485
+ "mysql") || file_contains?(".env", "mysql://")
486
+ databases << :sqlite if file_contains?("config/database.yml", "sqlite") || file_exists_in_project?("*.sqlite*")
487
+ databases << :mongodb if package_json_has_dependency?("mongoose") || requirements_contains?("pymongo")
488
+ databases << :redis if package_json_has_dependency?("redis") || requirements_contains?("redis")
489
+
490
+ databases.first || :unknown
491
+ end
492
+
493
+ # Detect sensitive files that should be handled carefully
494
+ def detect_sensitive_files
495
+ sensitive_patterns = %w[
496
+ config/master.key config/credentials/* .env .env.*
497
+ *.pem *.p12 *.jks .npmrc auth_token api_key
498
+ ]
499
+
500
+ found_files = []
501
+ sensitive_patterns.each do |pattern|
502
+ if pattern.include?("*")
503
+ found_files.concat(Dir.glob(File.join(@project_path, "**", pattern)))
504
+ else
505
+ file_path = File.join(@project_path, pattern)
506
+ found_files << file_path if File.exist?(file_path)
507
+ end
508
+ rescue Errno::EACCES, Errno::EIO, StandardError
509
+ # Skip patterns that cause errors
510
+ end
511
+
512
+ found_files.map { |f| Pathname.new(f).relative_path_from(Pathname.new(@project_path)).to_s }
513
+ end
514
+
515
+ # Suggest copy files rules based on project characteristics
516
+ def suggest_copy_files_rules(project_info)
517
+ files = []
518
+
519
+ case project_info[:type]
520
+ when :rails
521
+ files.push(
522
+ { "source" => "config/master.key", "strategy" => "copy", "required" => false },
523
+ { "source" => ".env", "strategy" => "symlink", "required" => false },
524
+ { "source" => ".env.development", "strategy" => "symlink", "required" => false }
525
+ )
526
+ when :nodejs, :nextjs, :react
527
+ files.push(
528
+ { "source" => ".env", "strategy" => "symlink", "required" => false },
529
+ { "source" => ".env.local", "strategy" => "symlink", "required" => false },
530
+ { "source" => ".npmrc", "strategy" => "copy", "required" => false }
531
+ )
532
+ when :python, :django
533
+ files.push(
534
+ { "source" => ".env", "strategy" => "symlink", "required" => false },
535
+ { "source" => "secrets.yml", "strategy" => "copy", "required" => false }
536
+ )
537
+ end
538
+
539
+ # Add any detected sensitive files
540
+ project_info[:sensitive_files].each do |file|
541
+ next if files.any? { |f| f["source"] == file }
542
+
543
+ strategy = file.match?(/\.(key|pem|p12|jks)$/) ? "copy" : "symlink"
544
+ files << { "source" => file, "strategy" => strategy, "required" => false }
545
+ end
546
+
547
+ { "type" => "copy_files", "config" => { "files" => files } }
548
+ end
549
+
550
+ # Suggest setup commands based on package manager
551
+ def suggest_setup_commands_rules(project_info)
552
+ commands = []
553
+
554
+ case project_info[:package_manager]
555
+ when :bundler
556
+ commands << { "command" => %w[bundle install], "description" => "Install Ruby dependencies" }
557
+ if project_info[:type] == :rails
558
+ commands << { "command" => ["bin/rails", "db:create"],
559
+ "condition" => "file_missing:db/development.sqlite3", "description" => "Create database" }
560
+ commands << { "command" => ["bin/rails", "db:migrate"], "description" => "Run database migrations" }
561
+ end
562
+ when :npm
563
+ commands << { "command" => %w[npm install], "description" => "Install Node.js dependencies" }
564
+ commands << { "command" => %w[npm run build], "condition" => "file_exists:package.json",
565
+ "required" => false, "description" => "Build project" }
566
+ when :yarn
567
+ commands << { "command" => %w[yarn install], "description" => "Install Node.js dependencies" }
568
+ commands << { "command" => %w[yarn build], "condition" => "file_exists:package.json", "required" => false,
569
+ "description" => "Build project" }
570
+ when :pnpm
571
+ commands << { "command" => %w[pnpm install], "description" => "Install Node.js dependencies" }
572
+ when :pip
573
+ commands << { "command" => ["pip", "install", "-r", "requirements.txt"],
574
+ "description" => "Install Python dependencies" }
575
+ when :pipenv
576
+ commands << { "command" => %w[pipenv install], "description" => "Install Python dependencies" }
577
+ when :poetry
578
+ commands << { "command" => %w[poetry install], "description" => "Install Python dependencies" }
579
+ end
580
+
581
+ { "type" => "setup_commands", "config" => { "commands" => commands } }
582
+ end
583
+
584
+ # Suggest template rules for documentation
585
+ def suggest_template_rules(project_info)
586
+ templates = []
587
+
588
+ # Always suggest session info template
589
+ templates << {
590
+ "source" => ".sxn/templates/session-info.md.liquid",
591
+ "destination" => "SESSION_INFO.md",
592
+ "required" => false
593
+ }
594
+
595
+ # Language-specific templates
596
+ case project_info[:type]
597
+ when :rails
598
+ templates << {
599
+ "source" => ".sxn/templates/rails/CLAUDE.md.liquid",
600
+ "destination" => "CLAUDE.md",
601
+ "required" => false
602
+ }
603
+ when :nodejs, :nextjs, :react
604
+ templates << {
605
+ "source" => ".sxn/templates/javascript/README.md.liquid",
606
+ "destination" => "README.md",
607
+ "required" => false,
608
+ "overwrite" => false
609
+ }
610
+ end
611
+
612
+ { "type" => "template", "config" => { "templates" => templates } }
613
+ end
614
+
615
+ # Check if project has typical Node.js characteristics
616
+ def has_nodejs_characteristics?
617
+ return false unless file_exists_in_project?("package.json")
618
+
619
+ # Check for Node.js-specific dependencies or scripts
620
+ nodejs_indicators = %w[
621
+ express fastify koa hapi
622
+ nodemon pm2 forever
623
+ @types/node typescript ts-node
624
+ eslint jest mocha nyc
625
+ webpack parcel rollup
626
+ commander inquirer chalk
627
+ axios request node-fetch
628
+ ]
629
+
630
+ # Check for Node.js specific scripts
631
+ nodejs_scripts = %w[start dev server build test]
632
+
633
+ has_nodejs_deps = nodejs_indicators.any? { |dep| package_json_has_dependency?(dep) }
634
+ has_nodejs_scripts = nodejs_scripts.any? { |script| package_json_has_script?(script) }
635
+ has_main_entry = package_json_has_main_entry?
636
+
637
+ has_nodejs_deps || has_nodejs_scripts || has_main_entry
638
+ end
639
+
640
+ # Helper methods for file content checking
641
+ def gemfile_contains?(gem_name)
642
+ gemfile_path = File.join(@project_path, "Gemfile")
643
+ return false unless File.exist?(gemfile_path)
644
+
645
+ File.read(gemfile_path).include?(gem_name)
646
+ rescue Errno::EACCES, Errno::EIO, StandardError
647
+ false
648
+ end
649
+
650
+ def package_json_has_dependency?(dep_name)
651
+ package_json_path = File.join(@project_path, "package.json")
652
+ return false unless File.exist?(package_json_path)
653
+
654
+ begin
655
+ package_data = JSON.parse(File.read(package_json_path))
656
+ all_deps = {}
657
+ all_deps.merge!(package_data["dependencies"] || {})
658
+ all_deps.merge!(package_data["devDependencies"] || {})
659
+ all_deps.merge!(package_data["peerDependencies"] || {})
660
+
661
+ all_deps.key?(dep_name)
662
+ rescue JSON::ParserError, Errno::EACCES, Errno::EIO, StandardError
663
+ false
664
+ end
665
+ end
666
+
667
+ def requirements_contains?(package_name)
668
+ requirements_path = File.join(@project_path, "requirements.txt")
669
+ return false unless File.exist?(requirements_path)
670
+
671
+ File.read(requirements_path).downcase.include?(package_name.downcase)
672
+ rescue Errno::EACCES, Errno::EIO, StandardError
673
+ false
674
+ end
675
+
676
+ def package_json_has_script?(script_name)
677
+ package_json_path = File.join(@project_path, "package.json")
678
+ return false unless File.exist?(package_json_path)
679
+
680
+ begin
681
+ package_data = JSON.parse(File.read(package_json_path))
682
+ scripts = package_data["scripts"] || {}
683
+ scripts.key?(script_name)
684
+ rescue JSON::ParserError
685
+ false
686
+ end
687
+ end
688
+
689
+ def package_json_has_main_entry?
690
+ package_json_path = File.join(@project_path, "package.json")
691
+ return false unless File.exist?(package_json_path)
692
+
693
+ begin
694
+ package_data = JSON.parse(File.read(package_json_path))
695
+ package_data.key?("main") || package_data.key?("module") || package_data.key?("exports")
696
+ rescue JSON::ParserError
697
+ false
698
+ end
699
+ end
700
+
701
+ def file_contains?(file_path, content)
702
+ full_path = File.join(@project_path, file_path)
703
+ return false unless File.exist?(full_path)
704
+
705
+ File.read(full_path).downcase.include?(content.downcase)
706
+ rescue StandardError
707
+ false
708
+ end
709
+
710
+ def env_contains?(env_var, content)
711
+ env_value = ENV.fetch(env_var, nil)
712
+ return false unless env_value
713
+
714
+ env_value.include?(content)
715
+ end
716
+
717
+ # Analysis methods for detailed project inspection
718
+ def analyze_important_files
719
+ important_patterns = %w[
720
+ README* LICENSE* CHANGELOG* CONTRIBUTING*
721
+ Dockerfile docker-compose.* .dockerignore
722
+ .gitignore .gitattributes
723
+ Makefile Rakefile
724
+ ]
725
+
726
+ found_files = []
727
+ important_patterns.each do |pattern|
728
+ found_files.concat(Dir.glob(File.join(@project_path, pattern), File::FNM_CASEFOLD))
729
+ end
730
+
731
+ found_files.map { |f| File.basename(f) }
732
+ end
733
+
734
+ def analyze_directory_structure
735
+ important_dirs = %w[
736
+ src lib app bin config test spec tests
737
+ public assets static dist build
738
+ docs documentation
739
+ ]
740
+
741
+ important_dirs.select do |dir|
742
+ File.directory?(File.join(@project_path, dir))
743
+ end
744
+ end
745
+
746
+ def analyze_dependencies
747
+ deps = {}
748
+
749
+ # Ruby dependencies
750
+ deps[:ruby] = parse_gemfile_lock if file_exists_in_project?("Gemfile.lock")
751
+
752
+ # Node.js dependencies
753
+ deps[:nodejs] = parse_package_json if file_exists_in_project?("package.json")
754
+
755
+ # Python dependencies
756
+ deps[:python] = parse_requirements_txt if file_exists_in_project?("requirements.txt")
757
+
758
+ deps
759
+ end
760
+
761
+ # Parse dependencies by type for testing
762
+ def parse_dependencies(type)
763
+ case type
764
+ when :bundler, :ruby
765
+ if file_exists_in_project?("Gemfile.lock")
766
+ parse_gemfile_lock
767
+ elsif file_exists_in_project?("Gemfile")
768
+ parse_gemfile
769
+ else
770
+ []
771
+ end
772
+ when :npm, :nodejs
773
+ file_exists_in_project?("package.json") ? parse_package_json : []
774
+ when :python
775
+ file_exists_in_project?("requirements.txt") ? parse_requirements_txt : []
776
+ else
777
+ []
778
+ end
779
+ end
780
+
781
+ def analyze_configuration_files
782
+ Dir.glob(File.join(@project_path, "**", "*.{yml,yaml,json,toml,ini,conf,config}"))
783
+ .map { |f| Pathname.new(f).relative_path_from(Pathname.new(@project_path)).to_s }
784
+ .select { |f| !f.start_with?("node_modules/") && !f.start_with?(".git/") }
785
+ end
786
+
787
+ def analyze_scripts
788
+ scripts = {}
789
+
790
+ # Package.json scripts
791
+ if file_exists_in_project?("package.json")
792
+ begin
793
+ package_data = JSON.parse(File.read(File.join(@project_path, "package.json")))
794
+ scripts[:npm] = package_data["scripts"]&.keys || []
795
+ rescue JSON::ParserError
796
+ # Ignore parsing errors
797
+ end
798
+ end
799
+
800
+ # Executable files
801
+ executable_files = Dir.glob(File.join(@project_path, "bin/*")).select { |f| File.executable?(f) }
802
+ scripts[:executables] = executable_files.map { |f| File.basename(f) }
803
+
804
+ scripts
805
+ end
806
+
807
+ def analyze_documentation
808
+ Dir.glob(File.join(@project_path, "**", "*.{md,txt,rst,adoc}"))
809
+ .map { |f| Pathname.new(f).relative_path_from(Pathname.new(@project_path)).to_s }
810
+ .select { |f| !f.start_with?("node_modules/") && !f.start_with?(".git/") }
811
+ end
812
+
813
+ # Dependency parsing helpers (simplified implementations)
814
+ def parse_gemfile_lock
815
+ # Simplified - would need more robust parsing for production
816
+ return [] unless file_exists_in_project?("Gemfile.lock")
817
+
818
+ begin
819
+ # Try to read and parse the file
820
+ content = File.read(File.join(@project_path, "Gemfile.lock"))
821
+ # Simplified parsing - just return a hardcoded list if content contains gem specs
822
+ content.include?("GEM") || content.include?("specs:") ? ["gems from Gemfile.lock"] : []
823
+ rescue StandardError
824
+ []
825
+ end
826
+ end
827
+
828
+ def parse_gemfile
829
+ # Parse Gemfile for gem dependencies
830
+ return [] unless file_exists_in_project?("Gemfile")
831
+
832
+ begin
833
+ content = File.read(File.join(@project_path, "Gemfile"))
834
+ gems = []
835
+
836
+ # Extract gem names using regex
837
+ content.scan(/gem\s+['"]([^'"]+)['"]/) do |match|
838
+ gems << match[0]
839
+ end
840
+
841
+ gems
842
+ rescue StandardError
843
+ []
844
+ end
845
+ end
846
+
847
+ def parse_package_json
848
+ return [] unless file_exists_in_project?("package.json")
849
+
850
+ begin
851
+ content = File.read(File.join(@project_path, "package.json"))
852
+ data = JSON.parse(content)
853
+
854
+ dependencies = []
855
+ dependencies.concat(data["dependencies"]&.keys || [])
856
+ dependencies.concat(data["devDependencies"]&.keys || [])
857
+ dependencies.concat(data["peerDependencies"]&.keys || [])
858
+
859
+ dependencies.uniq
860
+ rescue JSON::ParserError, StandardError
861
+ []
862
+ end
863
+ end
864
+
865
+ def parse_requirements_txt
866
+ # Simplified - would need more robust parsing for production
867
+ ["packages from requirements.txt"]
868
+ end
869
+ end
870
+ end
871
+ end