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.
- checksums.yaml +7 -0
- data/.gem_rbs_collection/addressable/2.8/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/addressable/2.8/addressable.rbs +62 -0
- data/.gem_rbs_collection/async/2.12/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/async/2.12/async.rbs +119 -0
- data/.gem_rbs_collection/async/2.12/kernel.rbs +5 -0
- data/.gem_rbs_collection/async/2.12/manifest.yaml +7 -0
- data/.gem_rbs_collection/bcrypt/3.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/bcrypt/3.1/bcrypt.rbs +47 -0
- data/.gem_rbs_collection/bcrypt/3.1/manifest.yaml +2 -0
- data/.gem_rbs_collection/bigdecimal/3.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/bigdecimal/3.1/bigdecimal-math.rbs +119 -0
- data/.gem_rbs_collection/bigdecimal/3.1/bigdecimal.rbs +1630 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +26 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +65 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
- data/.gem_rbs_collection/diff-lcs/1.5/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/diff-lcs/1.5/diff-lcs.rbs +11 -0
- data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
- data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
- data/.gem_rbs_collection/mini_mime/0.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/mini_mime/0.1/mini_mime.rbs +14 -0
- data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
- data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
- data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +822 -0
- data/.gem_rbs_collection/sqlite3/2.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/sqlite3/2.0/database.rbs +20 -0
- data/.gem_rbs_collection/sqlite3/2.0/pragmas.rbs +5 -0
- data/.rspec +4 -0
- data/.rubocop.yml +121 -0
- data/.simplecov +51 -0
- data/CHANGELOG.md +49 -0
- data/Gemfile +24 -0
- data/Gemfile.lock +329 -0
- data/LICENSE.txt +21 -0
- data/README.md +225 -0
- data/Rakefile +54 -0
- data/Steepfile +50 -0
- data/bin/sxn +6 -0
- data/lib/sxn/CLI.rb +275 -0
- data/lib/sxn/commands/init.rb +137 -0
- data/lib/sxn/commands/projects.rb +350 -0
- data/lib/sxn/commands/rules.rb +435 -0
- data/lib/sxn/commands/sessions.rb +300 -0
- data/lib/sxn/commands/worktrees.rb +416 -0
- data/lib/sxn/commands.rb +13 -0
- data/lib/sxn/config/config_cache.rb +295 -0
- data/lib/sxn/config/config_discovery.rb +242 -0
- data/lib/sxn/config/config_validator.rb +562 -0
- data/lib/sxn/config.rb +259 -0
- data/lib/sxn/core/config_manager.rb +290 -0
- data/lib/sxn/core/project_manager.rb +307 -0
- data/lib/sxn/core/rules_manager.rb +306 -0
- data/lib/sxn/core/session_manager.rb +336 -0
- data/lib/sxn/core/worktree_manager.rb +281 -0
- data/lib/sxn/core.rb +13 -0
- data/lib/sxn/database/errors.rb +29 -0
- data/lib/sxn/database/session_database.rb +691 -0
- data/lib/sxn/database.rb +24 -0
- data/lib/sxn/errors.rb +76 -0
- data/lib/sxn/rules/base_rule.rb +367 -0
- data/lib/sxn/rules/copy_files_rule.rb +346 -0
- data/lib/sxn/rules/errors.rb +28 -0
- data/lib/sxn/rules/project_detector.rb +871 -0
- data/lib/sxn/rules/rules_engine.rb +485 -0
- data/lib/sxn/rules/setup_commands_rule.rb +307 -0
- data/lib/sxn/rules/template_rule.rb +262 -0
- data/lib/sxn/rules.rb +148 -0
- data/lib/sxn/runtime_validations.rb +96 -0
- data/lib/sxn/security/secure_command_executor.rb +364 -0
- data/lib/sxn/security/secure_file_copier.rb +478 -0
- data/lib/sxn/security/secure_path_validator.rb +258 -0
- data/lib/sxn/security.rb +15 -0
- data/lib/sxn/templates/common/gitignore.liquid +99 -0
- data/lib/sxn/templates/common/session-info.md.liquid +58 -0
- data/lib/sxn/templates/errors.rb +36 -0
- data/lib/sxn/templates/javascript/README.md.liquid +59 -0
- data/lib/sxn/templates/javascript/session-info.md.liquid +206 -0
- data/lib/sxn/templates/rails/CLAUDE.md.liquid +78 -0
- data/lib/sxn/templates/rails/database.yml.liquid +31 -0
- data/lib/sxn/templates/rails/session-info.md.liquid +144 -0
- data/lib/sxn/templates/template_engine.rb +346 -0
- data/lib/sxn/templates/template_processor.rb +279 -0
- data/lib/sxn/templates/template_security.rb +410 -0
- data/lib/sxn/templates/template_variables.rb +713 -0
- data/lib/sxn/templates.rb +28 -0
- data/lib/sxn/ui/output.rb +103 -0
- data/lib/sxn/ui/progress_bar.rb +91 -0
- data/lib/sxn/ui/prompt.rb +116 -0
- data/lib/sxn/ui/table.rb +183 -0
- data/lib/sxn/ui.rb +12 -0
- data/lib/sxn/version.rb +5 -0
- data/lib/sxn.rb +63 -0
- data/rbs_collection.lock.yaml +180 -0
- data/rbs_collection.yaml +39 -0
- data/scripts/test.sh +31 -0
- data/sig/external/liquid.rbs +116 -0
- data/sig/external/thor.rbs +99 -0
- data/sig/external/tty.rbs +71 -0
- data/sig/sxn/cli.rbs +46 -0
- data/sig/sxn/commands/init.rbs +38 -0
- data/sig/sxn/commands/projects.rbs +72 -0
- data/sig/sxn/commands/rules.rbs +95 -0
- data/sig/sxn/commands/sessions.rbs +62 -0
- data/sig/sxn/commands/worktrees.rbs +82 -0
- data/sig/sxn/commands.rbs +6 -0
- data/sig/sxn/config/config_cache.rbs +67 -0
- data/sig/sxn/config/config_discovery.rbs +64 -0
- data/sig/sxn/config/config_validator.rbs +64 -0
- data/sig/sxn/config.rbs +74 -0
- data/sig/sxn/core/config_manager.rbs +67 -0
- data/sig/sxn/core/project_manager.rbs +52 -0
- data/sig/sxn/core/rules_manager.rbs +54 -0
- data/sig/sxn/core/session_manager.rbs +59 -0
- data/sig/sxn/core/worktree_manager.rbs +50 -0
- data/sig/sxn/core.rbs +87 -0
- data/sig/sxn/database/errors.rbs +37 -0
- data/sig/sxn/database/session_database.rbs +151 -0
- data/sig/sxn/database.rbs +83 -0
- data/sig/sxn/errors.rbs +89 -0
- data/sig/sxn/rules/base_rule.rbs +137 -0
- data/sig/sxn/rules/copy_files_rule.rbs +65 -0
- data/sig/sxn/rules/errors.rbs +33 -0
- data/sig/sxn/rules/project_detector.rbs +115 -0
- data/sig/sxn/rules/rules_engine.rbs +118 -0
- data/sig/sxn/rules/setup_commands_rule.rbs +60 -0
- data/sig/sxn/rules/template_rule.rbs +44 -0
- data/sig/sxn/rules.rbs +287 -0
- data/sig/sxn/runtime_validations.rbs +16 -0
- data/sig/sxn/security/secure_command_executor.rbs +63 -0
- data/sig/sxn/security/secure_file_copier.rbs +79 -0
- data/sig/sxn/security/secure_path_validator.rbs +30 -0
- data/sig/sxn/security.rbs +128 -0
- data/sig/sxn/templates/errors.rbs +43 -0
- data/sig/sxn/templates/template_engine.rbs +50 -0
- data/sig/sxn/templates/template_processor.rbs +44 -0
- data/sig/sxn/templates/template_security.rbs +62 -0
- data/sig/sxn/templates/template_variables.rbs +103 -0
- data/sig/sxn/templates.rbs +104 -0
- data/sig/sxn/ui/output.rbs +50 -0
- data/sig/sxn/ui/progress_bar.rbs +39 -0
- data/sig/sxn/ui/prompt.rbs +38 -0
- data/sig/sxn/ui/table.rbs +43 -0
- data/sig/sxn/ui.rbs +63 -0
- data/sig/sxn/version.rbs +5 -0
- data/sig/sxn.rbs +29 -0
- metadata +635 -0
data/lib/sxn/database.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "database/errors"
|
4
|
+
require_relative "database/session_database"
|
5
|
+
|
6
|
+
module Sxn
|
7
|
+
# Database layer for session storage and management
|
8
|
+
#
|
9
|
+
# This module provides SQLite-based storage for session metadata,
|
10
|
+
# replacing filesystem scanning with O(1) indexed lookups.
|
11
|
+
#
|
12
|
+
# Features:
|
13
|
+
# - High-performance SQLite with optimized indexes
|
14
|
+
# - ACID transactions with rollback support
|
15
|
+
# - Full-text search capabilities
|
16
|
+
# - JSON metadata storage
|
17
|
+
# - Connection pooling and concurrent access handling
|
18
|
+
# - Automatic schema migrations
|
19
|
+
#
|
20
|
+
# This module follows Ruby gem best practices by using explicit requires
|
21
|
+
# instead of autoload for better loading performance and dependency clarity.
|
22
|
+
module Database
|
23
|
+
end
|
24
|
+
end
|
data/lib/sxn/errors.rb
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sxn
|
4
|
+
# Base error class for all Sxn-specific errors
|
5
|
+
class Error < StandardError
|
6
|
+
attr_reader :exit_code
|
7
|
+
|
8
|
+
def initialize(message = nil, exit_code: 1)
|
9
|
+
super(message)
|
10
|
+
@exit_code = exit_code
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Configuration-related errors
|
15
|
+
class ConfigurationError < Error; end
|
16
|
+
|
17
|
+
# Session management errors
|
18
|
+
class SessionError < Error; end
|
19
|
+
class SessionNotFoundError < SessionError; end
|
20
|
+
class SessionAlreadyExistsError < SessionError; end
|
21
|
+
class SessionExistsError < SessionError; end
|
22
|
+
class SessionHasChangesError < SessionError; end
|
23
|
+
class InvalidSessionNameError < SessionError; end
|
24
|
+
class NoActiveSessionError < SessionError; end
|
25
|
+
|
26
|
+
# Project management errors
|
27
|
+
class ProjectError < Error; end
|
28
|
+
class ProjectNotFoundError < ProjectError; end
|
29
|
+
class ProjectAlreadyExistsError < ProjectError; end
|
30
|
+
class ProjectExistsError < ProjectError; end
|
31
|
+
class ProjectInUseError < ProjectError; end
|
32
|
+
class InvalidProjectNameError < ProjectError; end
|
33
|
+
class InvalidProjectPathError < ProjectError; end
|
34
|
+
|
35
|
+
# Git operation errors
|
36
|
+
class GitError < Error; end
|
37
|
+
class WorktreeError < GitError; end
|
38
|
+
class WorktreeExistsError < WorktreeError; end
|
39
|
+
class WorktreeNotFoundError < WorktreeError; end
|
40
|
+
class WorktreeCreationError < WorktreeError; end
|
41
|
+
class WorktreeRemovalError < WorktreeError; end
|
42
|
+
class BranchError < GitError; end
|
43
|
+
|
44
|
+
# Security-related errors
|
45
|
+
class SecurityError < Error; end
|
46
|
+
class PathValidationError < SecurityError; end
|
47
|
+
class CommandExecutionError < SecurityError; end
|
48
|
+
|
49
|
+
# Rule execution errors
|
50
|
+
class RuleError < Error; end
|
51
|
+
class RuleValidationError < RuleError; end
|
52
|
+
class RuleExecutionError < RuleError; end
|
53
|
+
class RuleNotFoundError < RuleError; end
|
54
|
+
class InvalidRuleTypeError < RuleError; end
|
55
|
+
class InvalidRuleConfigError < RuleError; end
|
56
|
+
|
57
|
+
# Generic validation and application errors
|
58
|
+
class ValidationError < Error; end
|
59
|
+
class ApplicationError < Error; end
|
60
|
+
class RollbackError < Error; end
|
61
|
+
|
62
|
+
# Template processing errors
|
63
|
+
class TemplateError < Error; end
|
64
|
+
class TemplateNotFoundError < TemplateError; end
|
65
|
+
class TemplateProcessingError < TemplateError; end
|
66
|
+
|
67
|
+
# Database errors
|
68
|
+
class DatabaseError < Error; end
|
69
|
+
class DatabaseConnectionError < DatabaseError; end
|
70
|
+
class DatabaseMigrationError < DatabaseError; end
|
71
|
+
|
72
|
+
# MCP server errors
|
73
|
+
class MCPError < Error; end
|
74
|
+
class MCPServerError < MCPError; end
|
75
|
+
class MCPValidationError < MCPError; end
|
76
|
+
end
|
@@ -0,0 +1,367 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sxn
|
4
|
+
module Rules
|
5
|
+
# BaseRule is the abstract base class for all rule types in the sxn system.
|
6
|
+
# It defines the common interface that all rules must implement and provides
|
7
|
+
# shared functionality for validation, dependency management, and error handling.
|
8
|
+
#
|
9
|
+
# Rules are the building blocks of session setup automation. They can copy files,
|
10
|
+
# execute commands, process templates, or perform other project initialization tasks.
|
11
|
+
#
|
12
|
+
# @example Implementing a custom rule
|
13
|
+
# class MyCustomRule < BaseRule
|
14
|
+
# def validate
|
15
|
+
# raise ValidationError, "Custom validation failed" unless valid?
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# def apply
|
19
|
+
# # Perform the rule's action
|
20
|
+
# track_change(:file_created, "/path/to/file")
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# def rollback
|
24
|
+
# # Undo the rule's action
|
25
|
+
# File.unlink("/path/to/file") if File.exist?("/path/to/file")
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
class BaseRule
|
30
|
+
# Rule execution states
|
31
|
+
module States
|
32
|
+
PENDING = :pending
|
33
|
+
VALIDATING = :validating
|
34
|
+
VALIDATED = :validated
|
35
|
+
APPLYING = :applying
|
36
|
+
APPLIED = :applied
|
37
|
+
ROLLING_BACK = :rolling_back
|
38
|
+
ROLLED_BACK = :rolled_back
|
39
|
+
FAILED = :failed
|
40
|
+
end
|
41
|
+
|
42
|
+
include States
|
43
|
+
|
44
|
+
attr_reader :name, :config, :project_path, :session_path, :state, :dependencies, :changes, :errors
|
45
|
+
|
46
|
+
# Initialize a new rule instance
|
47
|
+
#
|
48
|
+
# @param name [String] Unique name for this rule instance (old format) or project_path (new format)
|
49
|
+
# @param config_or_session_path [Hash|String] Rule configuration (old) or session_path (new)
|
50
|
+
# @param project_path [String] Absolute path to the project root (old format)
|
51
|
+
# @param session_path [String] Absolute path to the session directory (old format)
|
52
|
+
# @param dependencies [Array<String>] Names of rules this rule depends on
|
53
|
+
def initialize(arg1 = nil, arg2 = nil, arg3 = nil, arg4 = nil, dependencies: [])
|
54
|
+
# Handle both old and new initialization formats
|
55
|
+
if (arg1.is_a?(String) || arg1.nil?) && arg2.is_a?(Hash) && arg3.is_a?(String) && arg4.is_a?(String)
|
56
|
+
# Old format: (name, config, project_path, session_path, dependencies: [])
|
57
|
+
@name = arg1 || "base_rule"
|
58
|
+
@config = arg2.dup.freeze
|
59
|
+
@project_path = File.realpath(arg3)
|
60
|
+
@session_path = File.realpath(arg4)
|
61
|
+
elsif arg1.is_a?(Hash) && arg2.is_a?(String) && arg3.is_a?(String)
|
62
|
+
# Special format: (config, project_path, session_path, name)
|
63
|
+
@name = arg4 || "base_rule"
|
64
|
+
@config = arg1.dup.freeze
|
65
|
+
@project_path = File.realpath(arg2)
|
66
|
+
@session_path = File.realpath(arg3)
|
67
|
+
elsif arg1.is_a?(String) && arg2.is_a?(String)
|
68
|
+
# New format: (project_path, session_path, config = {}, dependencies: [])
|
69
|
+
@name = "base_rule"
|
70
|
+
# Store the config as-is for validation, only freeze if it's a Hash
|
71
|
+
@config = if arg3.nil?
|
72
|
+
{}.freeze
|
73
|
+
elsif arg3.is_a?(Hash)
|
74
|
+
arg3.dup.freeze
|
75
|
+
else
|
76
|
+
# Store non-hash config as-is for validation to catch
|
77
|
+
arg3
|
78
|
+
end
|
79
|
+
@project_path = File.realpath(arg1)
|
80
|
+
@session_path = File.realpath(arg2)
|
81
|
+
else
|
82
|
+
raise ArgumentError,
|
83
|
+
"Invalid arguments. Expected (name, config, project_path, session_path) or (project_path, session_path, config={})"
|
84
|
+
end
|
85
|
+
|
86
|
+
@dependencies = dependencies.freeze
|
87
|
+
@state = PENDING
|
88
|
+
@changes = []
|
89
|
+
@errors = []
|
90
|
+
@start_time = nil
|
91
|
+
@end_time = nil
|
92
|
+
|
93
|
+
validate_paths!
|
94
|
+
rescue Errno::ENOENT => e
|
95
|
+
raise ArgumentError, "Invalid path provided: #{e.message}"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Validate the rule configuration and dependencies
|
99
|
+
# This method should be overridden by subclasses to implement specific validation logic
|
100
|
+
#
|
101
|
+
# @return [Boolean] true if validation passes
|
102
|
+
# @raise [ValidationError] if validation fails
|
103
|
+
def validate
|
104
|
+
change_state!(VALIDATING)
|
105
|
+
|
106
|
+
begin
|
107
|
+
validate_config!
|
108
|
+
validate_dependencies!
|
109
|
+
validate_rule_specific!
|
110
|
+
|
111
|
+
change_state!(VALIDATED)
|
112
|
+
true
|
113
|
+
rescue StandardError => e
|
114
|
+
@errors << e
|
115
|
+
change_state!(FAILED)
|
116
|
+
raise
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Apply the rule's action
|
121
|
+
# This method must be overridden by subclasses to implement the actual rule logic
|
122
|
+
#
|
123
|
+
# @param context [Hash] Optional execution context
|
124
|
+
# @return [Boolean] true if application succeeds
|
125
|
+
# @raise [ApplicationError] if application fails
|
126
|
+
def apply(context = {})
|
127
|
+
raise NotImplementedError, "#{self.class} must implement #apply"
|
128
|
+
end
|
129
|
+
|
130
|
+
# Rollback the rule's changes
|
131
|
+
# This method should be overridden by subclasses to implement rollback logic
|
132
|
+
#
|
133
|
+
# @return [Boolean] true if rollback succeeds
|
134
|
+
# @raise [RollbackError] if rollback fails
|
135
|
+
def rollback
|
136
|
+
return true if @state == PENDING || @state == FAILED
|
137
|
+
|
138
|
+
change_state!(ROLLING_BACK)
|
139
|
+
|
140
|
+
begin
|
141
|
+
rollback_changes!
|
142
|
+
change_state!(ROLLED_BACK)
|
143
|
+
true
|
144
|
+
rescue StandardError => e
|
145
|
+
@errors << e
|
146
|
+
change_state!(FAILED)
|
147
|
+
raise Sxn::Rules::RollbackError, "Failed to rollback rule #{@name}: #{e.message}"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Check if this rule can be executed (all dependencies are satisfied)
|
152
|
+
#
|
153
|
+
# @param completed_rules [Array<String>] List of rule names that have been completed
|
154
|
+
# @return [Boolean] true if all dependencies are satisfied
|
155
|
+
def can_execute?(completed_rules)
|
156
|
+
@dependencies.all? { |dep| completed_rules.include?(dep) }
|
157
|
+
end
|
158
|
+
|
159
|
+
# Get rule execution duration in seconds
|
160
|
+
#
|
161
|
+
# @return [Float, nil] Execution duration or nil if not completed
|
162
|
+
def duration
|
163
|
+
return nil unless @start_time && @end_time
|
164
|
+
|
165
|
+
@end_time - @start_time
|
166
|
+
end
|
167
|
+
|
168
|
+
# Get rule type
|
169
|
+
#
|
170
|
+
# @return [String] Rule type based on class name
|
171
|
+
def type
|
172
|
+
self.class.name.split("::").last.downcase.gsub(/rule$/, "")
|
173
|
+
end
|
174
|
+
|
175
|
+
# Check if rule is required
|
176
|
+
#
|
177
|
+
# @return [Boolean] true if rule is required
|
178
|
+
def required?
|
179
|
+
true
|
180
|
+
end
|
181
|
+
|
182
|
+
# Validate rule configuration (public method expected by tests)
|
183
|
+
#
|
184
|
+
# @param config [Hash] Configuration to validate
|
185
|
+
# @return [Boolean] true if valid
|
186
|
+
def validate_config_hash(config = @config)
|
187
|
+
return true if config.nil? || config.empty?
|
188
|
+
|
189
|
+
config.is_a?(Hash)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Get rule description
|
193
|
+
#
|
194
|
+
# @return [String] Description of the rule
|
195
|
+
def description
|
196
|
+
"Base rule for #{type} operations"
|
197
|
+
end
|
198
|
+
|
199
|
+
# Check if rule has been successfully applied
|
200
|
+
#
|
201
|
+
# @return [Boolean] true if rule is in applied state
|
202
|
+
def applied?
|
203
|
+
@state == APPLIED
|
204
|
+
end
|
205
|
+
|
206
|
+
# Check if rule has failed
|
207
|
+
#
|
208
|
+
# @return [Boolean] true if rule is in failed state
|
209
|
+
def failed?
|
210
|
+
@state == FAILED
|
211
|
+
end
|
212
|
+
|
213
|
+
# Check if rule can be rolled back
|
214
|
+
#
|
215
|
+
# @return [Boolean] true if rule can be rolled back
|
216
|
+
def rollbackable?
|
217
|
+
@state == APPLIED && @changes.any?
|
218
|
+
end
|
219
|
+
|
220
|
+
# Get a hash representation of the rule for serialization
|
221
|
+
#
|
222
|
+
# @return [Hash] Rule data
|
223
|
+
def to_h
|
224
|
+
{
|
225
|
+
name: @name,
|
226
|
+
type: self.class.name.split("::").last,
|
227
|
+
state: @state,
|
228
|
+
config: @config,
|
229
|
+
dependencies: @dependencies,
|
230
|
+
changes: @changes.map(&:to_h),
|
231
|
+
errors: @errors.map(&:message),
|
232
|
+
duration: duration,
|
233
|
+
applied_at: @end_time&.iso8601
|
234
|
+
}
|
235
|
+
end
|
236
|
+
|
237
|
+
protected
|
238
|
+
|
239
|
+
# Track a change made by this rule for rollback purposes
|
240
|
+
#
|
241
|
+
# @param type [Symbol] Type of change (:file_created, :file_modified, :directory_created, etc.)
|
242
|
+
# @param target [String] Path or identifier of what was changed
|
243
|
+
# @param metadata [Hash] Additional metadata about the change
|
244
|
+
def track_change(type, target, metadata = {})
|
245
|
+
change = RuleChange.new(type, target, metadata)
|
246
|
+
@changes << change
|
247
|
+
change
|
248
|
+
end
|
249
|
+
|
250
|
+
# Get the logger instance
|
251
|
+
#
|
252
|
+
# @return [Logger] Logger for this rule
|
253
|
+
def logger
|
254
|
+
@logger ||= Sxn.logger
|
255
|
+
end
|
256
|
+
|
257
|
+
# Log a message with rule context
|
258
|
+
#
|
259
|
+
# @param level [Symbol] Log level (:debug, :info, :warn, :error)
|
260
|
+
# @param message [String] Message to log
|
261
|
+
# @param metadata [Hash] Additional metadata
|
262
|
+
def log(level, message, metadata = {})
|
263
|
+
logger.send(level, "[Rule:#{@name}] #{message}") do
|
264
|
+
metadata.merge(rule_name: @name, rule_type: self.class.name)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
private
|
269
|
+
|
270
|
+
# Change the rule state and track timing
|
271
|
+
def change_state!(new_state)
|
272
|
+
old_state = @state
|
273
|
+
@state = new_state
|
274
|
+
|
275
|
+
case new_state
|
276
|
+
when APPLYING
|
277
|
+
@start_time = Time.now
|
278
|
+
when APPLIED, FAILED, ROLLED_BACK
|
279
|
+
@end_time = Time.now
|
280
|
+
end
|
281
|
+
|
282
|
+
log(:debug, "State changed from #{old_state} to #{new_state}")
|
283
|
+
end
|
284
|
+
|
285
|
+
# Validate that required paths exist and are accessible
|
286
|
+
def validate_paths!
|
287
|
+
raise ArgumentError, "Project path is not a directory: #{@project_path}" unless File.directory?(@project_path)
|
288
|
+
|
289
|
+
raise ArgumentError, "Session path is not a directory: #{@session_path}" unless File.directory?(@session_path)
|
290
|
+
|
291
|
+
# Ensure session path is writable
|
292
|
+
return if File.writable?(@session_path)
|
293
|
+
|
294
|
+
raise ArgumentError, "Session path is not writable: #{@session_path}"
|
295
|
+
end
|
296
|
+
|
297
|
+
# Validate basic rule configuration
|
298
|
+
def validate_config!
|
299
|
+
raise ValidationError, "Config must be a Hash" unless @config.is_a?(Hash)
|
300
|
+
|
301
|
+
# Subclasses should override this method for specific validation
|
302
|
+
validate_rule_specific!
|
303
|
+
end
|
304
|
+
|
305
|
+
# Validate rule dependencies
|
306
|
+
def validate_dependencies!
|
307
|
+
@dependencies.each do |dep|
|
308
|
+
raise ValidationError, "Invalid dependency: #{dep.inspect}" unless dep.is_a?(String) && !dep.empty?
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
# Validate rule-specific configuration
|
313
|
+
# Override this method in subclasses
|
314
|
+
def validate_rule_specific!
|
315
|
+
# Default implementation does nothing
|
316
|
+
true
|
317
|
+
end
|
318
|
+
|
319
|
+
# Rollback all tracked changes in reverse order
|
320
|
+
def rollback_changes!
|
321
|
+
@changes.reverse_each(&:rollback)
|
322
|
+
@changes.clear
|
323
|
+
true
|
324
|
+
end
|
325
|
+
|
326
|
+
# Represents a single change made by a rule
|
327
|
+
class RuleChange
|
328
|
+
attr_reader :type, :target, :metadata, :timestamp
|
329
|
+
|
330
|
+
def initialize(type, target, metadata = {})
|
331
|
+
@type = type
|
332
|
+
@target = target
|
333
|
+
@metadata = metadata.freeze
|
334
|
+
@timestamp = Time.now
|
335
|
+
end
|
336
|
+
|
337
|
+
# Rollback this specific change
|
338
|
+
def rollback
|
339
|
+
case @type
|
340
|
+
when :file_created
|
341
|
+
FileUtils.rm_f(@target)
|
342
|
+
when :file_modified
|
343
|
+
FileUtils.mv(@metadata[:backup_path], @target) if @metadata[:backup_path] && File.exist?(@metadata[:backup_path])
|
344
|
+
when :directory_created
|
345
|
+
Dir.rmdir(@target) if File.directory?(@target) && Dir.empty?(@target)
|
346
|
+
when :symlink_created
|
347
|
+
File.unlink(@target) if File.symlink?(@target)
|
348
|
+
when :command_executed
|
349
|
+
# Command execution cannot be rolled back
|
350
|
+
# This is logged for audit purposes only
|
351
|
+
else
|
352
|
+
raise Sxn::Rules::RollbackError, "Unknown change type for rollback: #{@type}"
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
def to_h
|
357
|
+
{
|
358
|
+
type: @type,
|
359
|
+
target: @target,
|
360
|
+
metadata: @metadata,
|
361
|
+
timestamp: @timestamp.iso8601
|
362
|
+
}
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|