cline-rb 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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +139 -0
  3. data/README.md +1216 -0
  4. data/TODO.md +2 -0
  5. data/lib/cline/cli.rb +373 -0
  6. data/lib/cline/config.rb +100 -0
  7. data/lib/cline/configuration.rb +23 -0
  8. data/lib/cline/data.rb +119 -0
  9. data/lib/cline/file_content.rb +33 -0
  10. data/lib/cline/global_settings.rb +17 -0
  11. data/lib/cline/global_state/api_providers.rb +48 -0
  12. data/lib/cline/global_state/auto_approval.rb +73 -0
  13. data/lib/cline/global_state/browser.rb +52 -0
  14. data/lib/cline/global_state/features.rb +56 -0
  15. data/lib/cline/global_state/general.rb +77 -0
  16. data/lib/cline/global_state/models.rb +127 -0
  17. data/lib/cline/global_state/toggles.rb +33 -0
  18. data/lib/cline/global_state/workspace.rb +41 -0
  19. data/lib/cline/global_state.rb +16 -0
  20. data/lib/cline/log.rb +288 -0
  21. data/lib/cline/logs.rb +136 -0
  22. data/lib/cline/mcp_settings.rb +30 -0
  23. data/lib/cline/model.rb +47 -0
  24. data/lib/cline/models.rb +11 -0
  25. data/lib/cline/overlay_hash.rb +125 -0
  26. data/lib/cline/providers.rb +59 -0
  27. data/lib/cline/schema.rb +144 -0
  28. data/lib/cline/secret_string.rb +83 -0
  29. data/lib/cline/secrets.rb +119 -0
  30. data/lib/cline/serializable/cline_data.rb +131 -0
  31. data/lib/cline/serializable/dir.rb +81 -0
  32. data/lib/cline/serializable/file.rb +106 -0
  33. data/lib/cline/session.rb +87 -0
  34. data/lib/cline/session_data.rb +154 -0
  35. data/lib/cline/session_message.rb +178 -0
  36. data/lib/cline/session_messages.rb +61 -0
  37. data/lib/cline/sessions.rb +30 -0
  38. data/lib/cline/skill.rb +148 -0
  39. data/lib/cline/skills.rb +8 -0
  40. data/lib/cline/task.rb +75 -0
  41. data/lib/cline/task_message.rb +247 -0
  42. data/lib/cline/task_messages.rb +11 -0
  43. data/lib/cline/tasks.rb +30 -0
  44. data/lib/cline/usage.rb +37 -0
  45. data/lib/cline/utils/enumerable_dir_objects.rb +103 -0
  46. data/lib/cline/utils/file.rb +71 -0
  47. data/lib/cline/utils/file_monitor.rb +56 -0
  48. data/lib/cline/utils/logger.rb +37 -0
  49. data/lib/cline/utils/os/linux.rb +43 -0
  50. data/lib/cline/utils/os/mingw32.rb +46 -0
  51. data/lib/cline/utils/os.rb +31 -0
  52. data/lib/cline/utils/schema.rb +290 -0
  53. data/lib/cline/version.rb +6 -0
  54. data/lib/cline/workspace.rb +25 -0
  55. data/lib/cline/workspace_settings.rb +29 -0
  56. data/lib/cline/workspaces.rb +8 -0
  57. data/lib/cline.rb +22 -0
  58. metadata +249 -0
@@ -0,0 +1,144 @@
1
+ require 'json'
2
+ require 'shale'
3
+
4
+ module Cline
5
+ # Base class for any Cline domain object that defines some attributes that are serializable in JSON.
6
+ # Handle the following features:
7
+ # * Provide Shale attributes interface.
8
+ # * Automatically transforms Cline camelCase naming.
9
+ # * Keep track of extra attributes to serialize them back if needed.
10
+ class Schema < Shale::Mapper
11
+ # @!group Internal
12
+
13
+ # @return [Hash] Store all extra values from a JSON parse
14
+ attr_accessor :extra_attributes
15
+
16
+ class << self
17
+ # Define the attributes that are already in snake case in Cline files
18
+ #
19
+ # @param attributes [Array<Symbol>] List of attributes already in snake case
20
+ def cline_snake_attributes(*attributes)
21
+ @snake_attributes ||= []
22
+ @snake_attributes.concat(attributes)
23
+ @snake_attributes.uniq!
24
+ end
25
+
26
+ # Parse a Hash object and instantiate the proper instance from it.
27
+ #
28
+ # @param hash [Hash] Data
29
+ # @param args [Array] Remaining arguments to be transferred to Shale
30
+ # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
31
+ # @return [Schema] Corresponding instance
32
+ def of_hash(hash, *args, **kwargs)
33
+ complete_hash_mapping
34
+ known = hash_mapping.keys.keys
35
+ # Separate unknown attributes.
36
+ known_hash = {}
37
+ extra_hash = {}
38
+ hash.each do |key, value|
39
+ if known.include?(key)
40
+ known_hash[key] = value
41
+ else
42
+ extra_hash[key] = value
43
+ end
44
+ end
45
+ # Give Shale the data it knows about, without extra attributes
46
+ instance = super(known_hash, *args, **kwargs)
47
+ instance.extra_attributes = extra_hash unless extra_hash.empty?
48
+ instance
49
+ end
50
+
51
+ # Get a Hash object from an instance.
52
+ #
53
+ # @param instance [Schema] Object to serialize to a Hash
54
+ # @param args [Array] Remaining arguments to be transferred to Shale
55
+ # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
56
+ # @return [Hash] Corresponding hash
57
+ def as_hash(instance, *args, **kwargs)
58
+ complete_hash_mapping
59
+ hash = super
60
+ hash.merge!(instance.extra_attributes) if instance.extra_attributes
61
+ hash
62
+ end
63
+
64
+ # Cast an input value to this Schema object.
65
+ # Allow the attribute to be initialized directly using its Hash form.
66
+ #
67
+ # @param value [Schema, Hash, nil] The value that could be used to initialize a new instance of this attribute.
68
+ # @return [Schema, nil] The corresponding instance, or nil if none.
69
+ def cast(value)
70
+ return nil if value.nil?
71
+
72
+ # We expect the value to be either a Hash that can be used to initialize a new instance, or a new instance already initialized.
73
+ if value.is_a?(self)
74
+ value
75
+ elsif value.is_a?(Hash)
76
+ new(**value)
77
+ else
78
+ raise "Unable to cast #{value} into #{name}"
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ # Complete the hash mapping to include camelCase to snake_case as the defaults.
85
+ # Do it only once.
86
+ def complete_hash_mapping
87
+ return if @hash_mapping_completed
88
+
89
+ snake_attributes = @snake_attributes || []
90
+ # Find the hash name that we expect for each attribute name
91
+ attributes_mapping = attributes.keys.to_h do |attribute|
92
+ [
93
+ attribute,
94
+ if snake_attributes.include?(attribute)
95
+ attribute.to_s
96
+ else
97
+ attribute.to_s.gsub(/(?<!_)_([a-zA-Z0-9])(?!_)/) { Regexp.last_match(1).upcase }
98
+ end
99
+ ]
100
+ end
101
+
102
+ # Redefine the hash mapping for all expected attributes
103
+ hsh do
104
+ attributes_mapping.each do |attribute, hash_attribute|
105
+ # Find the hash name that we expect for each attribute name
106
+ map hash_attribute, to: attribute
107
+ end
108
+ end
109
+ @hash_mapping_completed = true
110
+ end
111
+ end
112
+
113
+ # Output this object as Cline JSON.
114
+ #
115
+ # @return [String] Cline JSON
116
+ def to_cline_json
117
+ JSON.dump(self.class.as_hash(self))
118
+ end
119
+
120
+ # Output this object as a Hash.
121
+ #
122
+ # @return [Hash] Cline JSON
123
+ def to_hash
124
+ hash = self.class.attributes.to_h do |attribute|
125
+ value = send(attribute.to_sym)
126
+ [
127
+ attribute,
128
+ value.respond_to?(:to_hash) ? value.to_hash : value
129
+ ]
130
+ end
131
+ hash.merge!(extra_attributes:) if extra_attributes
132
+ hash
133
+ end
134
+
135
+ # Equality check
136
+ #
137
+ # @param other [Object] The other to check equality with
138
+ # @return [Boolean] True if objects are equal
139
+ def ==(other)
140
+ other.is_a?(Schema) &&
141
+ other.to_hash == to_hash
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,83 @@
1
+ require 'secret_string'
2
+
3
+ module Cline
4
+ # ::SecretString wrapper that allows us to use it in our Shale schemas
5
+ class SecretString < Schema
6
+ extend Forwardable
7
+
8
+ # @!group Public API
9
+
10
+ def_delegators :secret_string, *%i[to_s to_unprotected]
11
+
12
+ # Equality check
13
+ #
14
+ # @param other [Object] The other to check equality with
15
+ # @return [Boolean] True if objects are equal
16
+ def ==(other)
17
+ other.is_a?(SecretString) &&
18
+ other.to_unprotected == to_unprotected
19
+ end
20
+
21
+ # @!group Internal
22
+
23
+ # Constructor
24
+ #
25
+ # @param unprotected_string [String] The unprotected string
26
+ def initialize(unprotected_string)
27
+ super()
28
+ @secret_string = ::SecretString.new(unprotected_string)
29
+ end
30
+
31
+ # @return [Hash] The internal secret string
32
+ attr_reader :secret_string
33
+
34
+ # Output this object as a Hash.
35
+ #
36
+ # @return [Hash] Cline JSON
37
+ def to_hash
38
+ SecretString.as_hash(self)
39
+ end
40
+
41
+ class << self
42
+ # @!group Internal
43
+
44
+ # Parse a Hash object and instantiate the proper instance from it.
45
+ #
46
+ # @param hash [Hash] Data
47
+ # @param _args [Array] Remaining arguments to be transferred to Shale
48
+ # @param _kwargs [Hash] Remaining kwargs to be transferred to Shale
49
+ # @return [Schema] Corresponding instance
50
+ def of_hash(hash, *_args, **_kwargs)
51
+ SecretString.new(hash)
52
+ end
53
+
54
+ # Get a Hash object from an instance.
55
+ #
56
+ # @param instance [Schema] Object to serialize to a Hash
57
+ # @param _args [Array] Remaining arguments to be transferred to Shale
58
+ # @param _kwargs [Hash] Remaining kwargs to be transferred to Shale
59
+ # @return [Hash] Corresponding hash
60
+ def as_hash(instance, *_args, **_kwargs)
61
+ instance.secret_string.to_unprotected
62
+ end
63
+
64
+ # Cast an input value to this Schema object.
65
+ # Allow the attribute to be initialized directly using its Hash form.
66
+ #
67
+ # @param value [Schema, String, nil] The value that could be used to initialize a new instance of this attribute.
68
+ # @return [Schema, nil] The corresponding instance, or nil if none.
69
+ def cast(value)
70
+ return nil if value.nil?
71
+
72
+ # We expect the value to be a String, or a new instance already initialized.
73
+ if value.is_a?(self)
74
+ value
75
+ elsif value.is_a?(String)
76
+ new(value)
77
+ else
78
+ raise "Unable to cast #{value} into #{name}"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,119 @@
1
+ module Cline
2
+ # Access secrets stored in the data directory's secrets.json file
3
+ class Secrets < Schema
4
+ # @!group Public API
5
+
6
+ Serializable::ClineData.include_for(self, 'secrets.json')
7
+
8
+ # @return [SecretString, nil] Cline API key
9
+ attribute :cline_api_key, SecretString
10
+
11
+ # @return [SecretString, nil] OpenAI API key
12
+ attribute :open_ai_api_key, SecretString
13
+
14
+ # @return [SecretString, nil] Gemini API key
15
+ attribute :gemini_api_key, SecretString
16
+
17
+ # @return [SecretString, nil] Generic API key
18
+ attribute :api_key, SecretString
19
+
20
+ # @return [SecretString, nil] AWS access key
21
+ attribute :aws_access_key, SecretString
22
+
23
+ # @return [SecretString, nil] AWS secret key
24
+ attribute :aws_secret_key, SecretString
25
+
26
+ # @return [SecretString, nil] AWS session token
27
+ attribute :aws_session_token, SecretString
28
+
29
+ # @return [SecretString, nil] DeepSeek API key
30
+ attribute :deep_seek_api_key, SecretString
31
+
32
+ # @return [SecretString, nil] OpenAI native API key
33
+ attribute :open_ai_native_api_key, SecretString
34
+
35
+ # @return [SecretString, nil] OpenRouter API key
36
+ attribute :open_router_api_key, SecretString
37
+
38
+ # @return [SecretString, nil] LiteLLM API key
39
+ attribute :lite_llm_api_key, SecretString
40
+
41
+ # @return [SecretString, nil] SAP AI Core client ID
42
+ attribute :sap_ai_core_client_id, SecretString
43
+
44
+ # @return [SecretString, nil] SAP AI Core client secret
45
+ attribute :sap_ai_core_client_secret, SecretString
46
+
47
+ # @return [SecretString, nil] Mistral API key
48
+ attribute :mistral_api_key, SecretString
49
+
50
+ # @return [SecretString, nil] ZAI API key
51
+ attribute :zai_api_key, SecretString
52
+
53
+ # @return [SecretString, nil] Groq API key
54
+ attribute :groq_api_key, SecretString
55
+
56
+ # @return [SecretString, nil] Cerebras API key
57
+ attribute :cerebras_api_key, SecretString
58
+
59
+ # @return [SecretString, nil] Vercel AI Gateway API key
60
+ attribute :vercel_ai_gateway_api_key, SecretString
61
+
62
+ # @return [SecretString, nil] Baseten API key
63
+ attribute :baseten_api_key, SecretString
64
+
65
+ # @return [SecretString, nil] Requesty API key
66
+ attribute :requesty_api_key, SecretString
67
+
68
+ # @return [SecretString, nil] Fireworks API key
69
+ attribute :fireworks_api_key, SecretString
70
+
71
+ # @return [SecretString, nil] Together API key
72
+ attribute :together_api_key, SecretString
73
+
74
+ # @return [SecretString, nil] Qwen API key
75
+ attribute :qwen_api_key, SecretString
76
+
77
+ # @return [SecretString, nil] Doubao API key
78
+ attribute :doubao_api_key, SecretString
79
+
80
+ # @return [SecretString, nil] Moonshot API key
81
+ attribute :moonshot_api_key, SecretString
82
+
83
+ # @return [SecretString, nil] HuggingFace API key
84
+ attribute :hugging_face_api_key, SecretString
85
+
86
+ # @return [SecretString, nil] Nebius API key
87
+ attribute :nebius_api_key, SecretString
88
+
89
+ # @return [SecretString, nil] AskSage API key
90
+ attribute :asksage_api_key, SecretString
91
+
92
+ # @return [SecretString, nil] xAI API key
93
+ attribute :xai_api_key, SecretString
94
+
95
+ # @return [SecretString, nil] Sambanova API key
96
+ attribute :sambanova_api_key, SecretString
97
+
98
+ # @return [SecretString, nil] Huawei Cloud Maas API key
99
+ attribute :huawei_cloud_maas_api_key, SecretString
100
+
101
+ # @return [SecretString, nil] Dify API key
102
+ attribute :dify_api_key, SecretString
103
+
104
+ # @return [SecretString, nil] Minimax API key
105
+ attribute :minimax_api_key, SecretString
106
+
107
+ # @return [SecretString, nil] Hicap API key
108
+ attribute :hicap_api_key, SecretString
109
+
110
+ # @return [SecretString, nil] AIHubMix API key
111
+ attribute :aihubmix_api_key, SecretString
112
+
113
+ # @return [SecretString, nil] Nous Research API key
114
+ attribute :nous_research_api_key, SecretString
115
+
116
+ # @return [SecretString, nil] Weights & Biases API key
117
+ attribute :wandb_api_key, SecretString
118
+ end
119
+ end
@@ -0,0 +1,131 @@
1
+ module Cline
2
+ # List of mixins that provide serialization/deserialization features on other objects.
3
+ module Serializable
4
+ # Add features to initialize from and save an object to Cline data.
5
+ # Cline data is defined as a JSON file, relative to a base directory.
6
+ # This mixin is intended to be included using the `.include_for(self, cline_json_file)` method.
7
+ #
8
+ # Provides:
9
+ # - `.from_cline_data(base_dir) -> [Object, nil]` Provides a new instance initialized from a Cline JSON file present in a base directory.
10
+ # - `.monitor_cline_data_changes(base_dir, on_change)` Provides a monitor to be notified on Cline data changes.
11
+ # - `#to_cline_data(base_dir)` Save an instance in the Cline data.
12
+ #
13
+ # Requires:
14
+ # - `.of_hash(hash) -> Object` The deserializer that returns an instance from a JSON object.
15
+ # - `#to_cline_json -> String` The serializer that returns a JSON string from the instance.
16
+ module ClineData
17
+ # @!group Public API
18
+
19
+ # Save the instance into the Cline data
20
+ def save
21
+ raise 'This instance has not been initialized from a Cline file' unless file
22
+
23
+ FileUtils.mkdir_p(::File.dirname(file))
24
+ ::File.write(file, to_cline_json)
25
+ end
26
+
27
+ # @!group Internal
28
+
29
+ # Class methods that should be made accessible to any class including our mixin
30
+ module ClassMethods
31
+ # @!group Internal
32
+
33
+ # Instantiate an instance of the including class from a base directory.
34
+ #
35
+ # @param base_dir [String] Base directory used to initialize the new instance
36
+ # @param args [Array] Extra parameters to give to the instance's constructor
37
+ # @param create [Boolean] Should data be created if it does not exist?
38
+ # @param kwargs [Hash] Extra kwargs to give to the instance's constructor
39
+ # @return [Object, nil] The instance, or nil if no Cline data exists
40
+ def from_cline_data(base_dir, *args, create: false, **kwargs)
41
+ instance = self.open(
42
+ ::File.join(base_dir, cline_json_file(base_dir)),
43
+ *args,
44
+ default: create ? '{}' : nil,
45
+ **kwargs
46
+ )
47
+ return unless instance
48
+
49
+ instance.initialize_from_dir(base_dir, create:)
50
+ instance
51
+ end
52
+
53
+ # Monitor changes done on the file and call a callback for each update.
54
+ #
55
+ # @param base_dir [String] Base directory used to initialize the new instance
56
+ # @param args [Array] Extra parameters to give to the instance's constructor
57
+ # @param on_change [#call] Block called each time there is an update.
58
+ # * Param instance [Object, nil] New instance with updates, or nil if no instance
59
+ # @param monitoring_interval_secs [Float] The monitoring interval in seconds
60
+ # @param kwargs [Hash] Extra kwargs to give to the instance's constructor
61
+ # @yield Optional code called while monitoring is in place.
62
+ # If used then monitoring is stopped at the end of the block's execution.
63
+ # @return [Utils::FileMonitor, nil] If no block has been given, return the monitor that needs to be
64
+ # stopped by the caller when monitoring should end.
65
+ def monitor_cline_data_changes(base_dir, *args, on_change:, monitoring_interval_secs: 1, **kwargs, &)
66
+ monitor_updates(
67
+ ::File.join(base_dir, cline_json_file(base_dir)),
68
+ *args,
69
+ on_change: proc do |instance|
70
+ instance.initialize_from_dir(base_dir, create: false)
71
+ on_change.call(instance)
72
+ end,
73
+ monitoring_interval_secs:,
74
+ **kwargs,
75
+ &
76
+ )
77
+ end
78
+
79
+ # Default factory for instances.
80
+ # This could be overriden by some classes that need to instantiate differently.
81
+ #
82
+ # @param file [String] File to initialize from.
83
+ # @param args [Array] Extra parameters to give to the instance's constructor.
84
+ # @param kwargs [Hash] Extra kwargs to give to the instance's constructor.
85
+ # @return [Object] A new instance.
86
+ def new_instance(file, *args, **kwargs)
87
+ of_hash(Utils::File.safe_json_read(file), *args, **kwargs)
88
+ end
89
+
90
+ private
91
+
92
+ # Get the relative JSON file path from the base directory
93
+ #
94
+ # @param base_dir [String] The base directory
95
+ # @return [String] The relative JSON file path
96
+ def cline_json_file(base_dir)
97
+ cline_json_file_def.is_a?(Proc) ? cline_json_file_def.call(base_dir) : cline_json_file_def
98
+ end
99
+ end
100
+
101
+ # Include the mixin and configure it with the JSON file path
102
+ #
103
+ # @param calling_class [Class] The class that is calling this method.
104
+ # @param cline_json_file_def [String, #call] The definition of the relative JSON file path to use. Can be one of:
105
+ # - [String] The static relative JSON file path to be used.
106
+ # - [#call] A proc that can devise dynamically the relative file path from the base directory:
107
+ # - Param base_dir [String] The base directory from which the JSON file is searched.
108
+ # - Return [String] The corresponding relative JSON file path from base_dir.
109
+ def self.include_for(calling_class, cline_json_file_def)
110
+ calling_class.class_eval do
111
+ include Dir
112
+ include File
113
+ include ClineData
114
+
115
+ class << self
116
+ # @return [String] The relative JSON file path
117
+ attr_accessor :cline_json_file_def
118
+ end
119
+ end
120
+ calling_class.cline_json_file_def = cline_json_file_def
121
+ end
122
+
123
+ # Hook used when this mixin is included in a base class
124
+ #
125
+ # @param base [Class] The base class
126
+ def self.included(base)
127
+ base.extend(ClassMethods)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,81 @@
1
+ require 'fileutils'
2
+
3
+ module Cline
4
+ module Serializable
5
+ # Add features to initialize from and save an object to a directory.
6
+ #
7
+ # Provides:
8
+ # - `.open(dir) -> [Object, nil]` Provides a new instance initialized from the directory, or nil if no directory.
9
+ # - `#dir -> [String]` The directory from which this object was initialized.
10
+ # - `#subpath(path) -> [String]` Provide a sub-path from the directory the object was initialized from.
11
+ module Dir
12
+ # Class methods that should be made accessible to any class including our mixin
13
+ module ClassMethods
14
+ # @!group Public API
15
+
16
+ # Instantiate an instance of the including class from a given directory.
17
+ #
18
+ # @param dir [String] Directory used to initialize the new instance
19
+ # @param args [Array] Extra parameters to give to the instance's constructor
20
+ # @param create [Boolean] Should the directory be created if it does not exist?
21
+ # @param kwargs [Hash] Extra kwargs to give to the instance's constructor
22
+ # @return [Object, nil] The instance initialized from this directory, or nil if none
23
+ def open(dir, *args, create: false, **kwargs)
24
+ unless ::File.exist?(dir) && ::File.directory?(dir)
25
+ return unless create
26
+
27
+ FileUtils.mkdir_p dir
28
+ end
29
+ instance = new_instance(dir, *args, **kwargs)
30
+ instance.initialize_from_dir(dir, create:)
31
+ instance
32
+ end
33
+
34
+ # @!group Internal
35
+
36
+ # Default factory for instances.
37
+ # This could be overriden by some classes that need to instantiate differently.
38
+ #
39
+ # @param _dir [String] The directory to create the instance for.
40
+ # @param args [Array] Extra parameters to give to the instance's constructor.
41
+ # @param kwargs [Hash] Extra kwargs to give to the instance's constructor.
42
+ # @return [Object] A new instance.
43
+ def new_instance(_dir, *args, **kwargs)
44
+ new(*args, **kwargs)
45
+ end
46
+ end
47
+
48
+ # @!group Internal
49
+
50
+ # Hook used when this mixin is included in a base class
51
+ #
52
+ # @param base [Class] The base class
53
+ def self.included(base)
54
+ base.extend(ClassMethods)
55
+ end
56
+
57
+ # @return [String] The directory used for the object's initialization
58
+ attr_reader :dir
59
+
60
+ # @return [Boolean] Should data be created if it does not exist?
61
+ attr_reader :create
62
+
63
+ # Initialize this instance from a directory
64
+ #
65
+ # @param dir [String] The directory to be used to initialize this instance
66
+ # @param create [Boolean] Should data be created if it does not exist?
67
+ def initialize_from_dir(dir, create:)
68
+ @dir = dir
69
+ @create = create
70
+ end
71
+
72
+ # Return the path to a sub-path of our instance directory
73
+ #
74
+ # @param path [String] The relative sub-path
75
+ # @return [String] The full path to the sub-path
76
+ def subpath(path)
77
+ ::File.join(dir, path)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,106 @@
1
+ require 'fileutils'
2
+
3
+ module Cline
4
+ module Serializable
5
+ # Add features to initialize from and save an object to a file.
6
+ #
7
+ # Provides:
8
+ # - `.open(file) -> [Object, nil]` Provides a new instance initialized from the file, or nil if no file.
9
+ # - `.monitor_file_changes(file, on_change)` Provides a monitor to be notified on file changes.
10
+ # - `.monitor_updates(file, on_change)` Provides a monitor to be notified on new instances upon updates.
11
+ # - `#file -> [String]` The file from which this object was initialized.
12
+ module File
13
+ # @!group Internal
14
+
15
+ # Class methods that should be made accessible to any class including our mixin
16
+ module ClassMethods
17
+ # Instantiate an instance of the including class from a given file.
18
+ #
19
+ # @param file [String] File path used to initialize the new instance
20
+ # @param args [Array] Extra parameters to give to the instance's constructor
21
+ # @param default [String, nil] Default file content to be created, or nil to only read existing one
22
+ # @param kwargs [Hash] Extra kwargs to give to the instance's constructor
23
+ # @return [Object, nil] The instance, or nil if no file exists
24
+ def open(file, *args, default: nil, **kwargs)
25
+ unless ::File.exist?(file)
26
+ return unless default
27
+
28
+ FileUtils.mkdir_p(::File.dirname(file))
29
+ ::File.write(file, default)
30
+ end
31
+
32
+ instance = new_instance(file, *args, **kwargs)
33
+ instance.initialize_from_file(file)
34
+ instance
35
+ end
36
+
37
+ # Monitor changes done on the file and call a callback for each update.
38
+ #
39
+ # @param file [String] File path to be monitored
40
+ # @param on_change [#call] Block called each time there is an update.
41
+ # * Param mtime [Time, nil] New file modification time, or nil if no file
42
+ # @param monitoring_interval_secs [Float] The monitoring interval in seconds
43
+ # @yield Optional code called while monitoring is in place.
44
+ # If used then monitoring is stopped at the end of the block's execution.
45
+ # @return [Utils::FileMonitor, nil] If no block has been given, return the monitor that needs to be
46
+ # stopped by the caller when monitoring should end.
47
+ def monitor_file_changes(file, on_change:, monitoring_interval_secs: 1, &)
48
+ monitor = Utils::FileMonitor.new(file, on_change:, monitoring_interval_secs:)
49
+ monitor.start(&)
50
+ monitor unless block_given?
51
+ end
52
+
53
+ # Monitor changes and call a callback for each update on the updated instance.
54
+ #
55
+ # @param file [String] File path to be monitored
56
+ # @param args [Array] Extra parameters to give to the instance's constructor
57
+ # @param on_change [#call] Block called each time there is an update.
58
+ # * Param instance [Object, nil] New instance with updates, or nil if no instance
59
+ # @param monitoring_interval_secs [Float] The monitoring interval in seconds
60
+ # @param kwargs [Hash] Extra kwargs to give to the instance's constructor
61
+ # @yield Optional code called while monitoring is in place.
62
+ # If used then monitoring is stopped at the end of the block's execution.
63
+ # @return [Utils::FileMonitor, nil] If no block has been given, return the monitor that needs to be
64
+ # stopped by the caller when monitoring should end.
65
+ def monitor_updates(file, *args, on_change:, monitoring_interval_secs: 1, **kwargs, &)
66
+ monitor_file_changes(
67
+ file,
68
+ on_change: proc do |_mtime|
69
+ on_change.call(self.open(file, *args, default: nil, **kwargs))
70
+ end,
71
+ monitoring_interval_secs:,
72
+ &
73
+ )
74
+ end
75
+
76
+ # Default factory for instances.
77
+ # This could be overriden by some classes that need to instantiate differently.
78
+ #
79
+ # @param _file [String] File to initialize from.
80
+ # @param args [Array] Extra parameters to give to the instance's constructor.
81
+ # @param kwargs [Hash] Extra kwargs to give to the instance's constructor.
82
+ # @return [Object] A new instance.
83
+ def new_instance(_file, *args, **kwargs)
84
+ new(*args, **kwargs)
85
+ end
86
+ end
87
+
88
+ # Hook used when this mixin is included in a base class
89
+ #
90
+ # @param base [Class] The base class
91
+ def self.included(base)
92
+ base.extend(ClassMethods)
93
+ end
94
+
95
+ # @return [String] The file used for the object's initialization
96
+ attr_reader :file
97
+
98
+ # Initialize this instance from a file
99
+ #
100
+ # @param file [String] The file to be used to initialize this instance
101
+ def initialize_from_file(file)
102
+ @file = file
103
+ end
104
+ end
105
+ end
106
+ end