openclacky 0.9.28 → 0.9.30

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/docs/deploy-architecture.md +619 -0
  4. data/lib/clacky/agent/llm_caller.rb +14 -2
  5. data/lib/clacky/agent/message_compressor.rb +24 -6
  6. data/lib/clacky/agent/message_compressor_helper.rb +17 -10
  7. data/lib/clacky/agent/session_serializer.rb +69 -0
  8. data/lib/clacky/agent/skill_manager.rb +2 -2
  9. data/lib/clacky/agent.rb +3 -0
  10. data/lib/clacky/brand_config.rb +29 -3
  11. data/lib/clacky/clacky_auth_client.rb +152 -0
  12. data/lib/clacky/clacky_cloud_config.rb +123 -0
  13. data/lib/clacky/cli.rb +13 -0
  14. data/lib/clacky/client.rb +21 -7
  15. data/lib/clacky/cloud_project_client.rb +169 -0
  16. data/lib/clacky/default_agents/base_prompt.md +1 -0
  17. data/lib/clacky/default_parsers/doc_parser.rb +9 -9
  18. data/lib/clacky/default_skills/browser-setup/SKILL.md +9 -0
  19. data/lib/clacky/default_skills/channel-setup/SKILL.md +21 -4
  20. data/lib/clacky/default_skills/channel-setup/feishu_setup.rb +8 -2
  21. data/lib/clacky/default_skills/deploy/SKILL.md +96 -5
  22. data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +1268 -274
  23. data/lib/clacky/default_skills/deploy/tools/create_database_service.rb +341 -0
  24. data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +72 -147
  25. data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +60 -50
  26. data/lib/clacky/default_skills/deploy/tools/list_services.rb +47 -60
  27. data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +147 -96
  28. data/lib/clacky/default_skills/new/SKILL.md +117 -5
  29. data/lib/clacky/default_skills/new/scripts/cloud_project_init.sh +74 -0
  30. data/lib/clacky/default_skills/new/scripts/create_rails_project.sh +32 -0
  31. data/lib/clacky/deploy_api_client.rb +484 -0
  32. data/lib/clacky/json_ui_controller.rb +16 -10
  33. data/lib/clacky/message_format/bedrock.rb +3 -2
  34. data/lib/clacky/message_history.rb +8 -0
  35. data/lib/clacky/plain_ui_controller.rb +1 -6
  36. data/lib/clacky/providers.rb +23 -4
  37. data/lib/clacky/server/browser_manager.rb +3 -1
  38. data/lib/clacky/server/channel/adapters/feishu/ws_client.rb +2 -1
  39. data/lib/clacky/server/channel/adapters/wecom/ws_client.rb +3 -1
  40. data/lib/clacky/server/channel/adapters/weixin/adapter.rb +5 -5
  41. data/lib/clacky/server/http_server.rb +12 -2
  42. data/lib/clacky/server/server_master.rb +43 -7
  43. data/lib/clacky/server/web_ui_controller.rb +17 -9
  44. data/lib/clacky/skill.rb +6 -2
  45. data/lib/clacky/tools/run_project.rb +4 -1
  46. data/lib/clacky/tools/shell.rb +7 -1
  47. data/lib/clacky/ui2/ui_controller.rb +1 -5
  48. data/lib/clacky/ui_interface.rb +5 -7
  49. data/lib/clacky/utils/arguments_parser.rb +22 -5
  50. data/lib/clacky/version.rb +1 -1
  51. data/lib/clacky/web/app.css +45 -5
  52. data/lib/clacky/web/app.js +126 -19
  53. data/lib/clacky/web/i18n.js +57 -0
  54. data/lib/clacky/web/sessions.js +108 -39
  55. data/lib/clacky/web/skills.js +8 -2
  56. data/lib/clacky.rb +3 -0
  57. metadata +8 -1
@@ -1,66 +1,76 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+
3
7
  module Clacky
4
8
  module DeployTools
5
- # Fetch runtime logs from deployed service
9
+ # Fetch runtime (environment) logs from a deployed Railway service via
10
+ # the Clacky Deploy API SSE endpoint.
11
+ #
12
+ # Uses DeployApiClient#stream_build_logs for build-phase logs (on failure),
13
+ # and this class for runtime log fetching (e.g. post-deploy diagnostics).
6
14
  class FetchRuntimeLogs
7
- DEFAULT_LINES = 100
8
- MAX_LINES = 1000
15
+ DEFAULT_LINES = 50
16
+ MAX_LINES = 200
17
+ STREAM_TIMEOUT = 60 # seconds
9
18
 
10
- # Fetch runtime logs
19
+ # Fetch recent runtime logs for a project.
11
20
  #
12
- # @param service_name [String] Service to fetch logs from
13
- # @param lines [Integer] Number of lines to fetch (default: 100)
14
- # @return [Hash] Result containing logs
15
- def self.execute(service_name:, lines: DEFAULT_LINES)
16
- if service_name.nil? || service_name.empty?
17
- return {
18
- error: "Service name is required",
19
- details: "Please provide a valid service name"
20
- }
21
- end
21
+ # @param project_id [String] Clacky project ID
22
+ # @param workspace_key [String] clacky_ak_* key
23
+ # @param base_url [String] API base URL
24
+ # @param lines [Integer] max log lines to collect
25
+ # @param keyword [String] optional filter keyword
26
+ # @return [Hash] { success: true, logs: Array<String> }
27
+ # or { success: false, error: String }
28
+ def self.execute(project_id:, workspace_key:, base_url:,
29
+ lines: DEFAULT_LINES, keyword: nil)
30
+ lines = [[lines.to_i, 1].max, MAX_LINES].min
22
31
 
23
- # Validate lines parameter
24
- lines = lines.to_i
25
- if lines <= 0 || lines > MAX_LINES
26
- return {
27
- error: "Invalid lines parameter",
28
- details: "Lines must be between 1 and #{MAX_LINES}",
29
- provided: lines
30
- }
31
- end
32
+ params = "project_id=#{URI.encode_www_form_component(project_id)}"
33
+ params += "&keyword=#{URI.encode_www_form_component(keyword)}" if keyword
32
34
 
33
- puts "📋 Fetching #{lines} lines of logs for service: #{service_name}"
35
+ uri = URI.parse(
36
+ "#{base_url.to_s.sub(%r{/+$}, "")}" \
37
+ "/openclacky/v1/deploy/logs/environment/stream?#{params}"
38
+ )
34
39
 
35
- # Execute command
36
- command = "clackycli logs -s #{shell_escape(service_name)} --lines #{lines}"
37
- output = `#{command} 2>&1`
38
- exit_code = $?.exitstatus
40
+ collected = []
39
41
 
40
- if exit_code != 0
41
- return {
42
- error: "Failed to fetch logs",
43
- details: output,
44
- exit_code: exit_code,
45
- service: service_name
46
- }
47
- end
42
+ http = Net::HTTP.new(uri.host, uri.port)
43
+ http.use_ssl = (uri.scheme == "https")
44
+ http.open_timeout = 10
45
+ http.read_timeout = STREAM_TIMEOUT
48
46
 
49
- {
50
- success: true,
51
- service: service_name,
52
- lines_requested: lines,
53
- logs: output,
54
- timestamp: Time.now.iso8601
55
- }
56
- end
47
+ req = Net::HTTP::Get.new(uri.request_uri)
48
+ req["Authorization"] = "Bearer #{workspace_key}"
49
+ req["Accept"] = "text/event-stream"
57
50
 
58
- # Escape shell arguments
59
- #
60
- # @param str [String] String to escape
61
- # @return [String] Escaped string
62
- def self.shell_escape(str)
63
- "'#{str.gsub("'", "'\\\\''")}'"
51
+ http.request(req) do |response|
52
+ response.read_body do |chunk|
53
+ chunk.split("\n").each do |raw|
54
+ next unless raw.start_with?("data:")
55
+
56
+ json_str = raw.sub(/\Adata:\s*/, "")
57
+ next if json_str.strip.empty?
58
+
59
+ begin
60
+ event = JSON.parse(json_str)
61
+ msg = event["message"].to_s
62
+ collected << msg unless msg.empty?
63
+ return { success: true, logs: collected } if collected.size >= lines
64
+ rescue JSON::ParserError
65
+ # skip malformed events
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ { success: true, logs: collected }
72
+ rescue => e
73
+ { success: false, error: "Failed to fetch runtime logs: #{e.message}" }
64
74
  end
65
75
  end
66
76
  end
@@ -1,79 +1,66 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "open3"
4
+ require "json"
5
+
3
6
  module Clacky
4
7
  module DeployTools
5
- # List Railway services with environment variables (sensitive data masked)
8
+ # List Railway services for the linked project.
9
+ # Uses RAILWAY_TOKEN passed through environment — no clackycli wrapper needed.
10
+ #
11
+ # NOTE: In the new deploy flow, service discovery is primarily done via the
12
+ # Clacky Deploy API (deploy/services endpoint). This tool is kept as a
13
+ # fallback for detecting the main service name via `railway service list`.
6
14
  class ListServices
7
- SENSITIVE_PATTERNS = [
8
- /password/i,
9
- /secret/i,
10
- /api_key/i,
11
- /token/i,
12
- /credential/i,
13
- /private_key/i
14
- ].freeze
15
15
 
16
- # Execute the list_services command
16
+ # List services for the current Railway project.
17
17
  #
18
- # @return [Hash] Result containing services array
19
- def self.execute
20
- output = `clackycli service list --json 2>&1`
21
- exit_code = $?.exitstatus
18
+ # @param platform_token [String] RAILWAY_TOKEN for this deploy task
19
+ # @return [Hash] {
20
+ # success: Boolean,
21
+ # services: Array<Hash>,
22
+ # main_service: Hash | nil, # first non-middleware service
23
+ # db_service: Hash | nil # first postgres/mysql service
24
+ # }
25
+ def self.execute(platform_token:)
26
+ if platform_token.nil? || platform_token.strip.empty?
27
+ return { success: false, error: "platform_token is required" }
28
+ end
29
+
30
+ env = ENV.to_h.merge("RAILWAY_TOKEN" => platform_token)
31
+ out, err, status = Open3.capture3(env, "railway status --json")
22
32
 
23
- if exit_code != 0
33
+ unless status.success?
24
34
  return {
25
- error: "Failed to list services",
26
- details: output,
27
- exit_code: exit_code
35
+ success: false,
36
+ error: "railway service list failed (exit #{status.exitstatus})",
37
+ details: err
28
38
  }
29
39
  end
30
40
 
31
- begin
32
- services = JSON.parse(output)
33
- masked_services = mask_sensitive_data(services)
34
-
35
- {
36
- success: true,
37
- services: masked_services,
38
- count: masked_services.length
39
- }
40
- rescue JSON::ParserError => e
41
- {
42
- error: "Failed to parse Railway CLI output",
43
- details: e.message,
44
- raw_output: output
45
- }
46
- end
47
- end
41
+ info = JSON.parse(out)
42
+ services = info["services"] || []
48
43
 
49
- # Mask sensitive environment variable values
50
- #
51
- # @param services [Array<Hash>] Array of service objects
52
- # @return [Array<Hash>] Services with masked sensitive data
53
- def self.mask_sensitive_data(services)
54
- services.map do |service|
55
- service = service.dup
56
-
57
- if service['variables']
58
- service['variables'] = mask_variables(service['variables'])
59
- end
60
-
61
- service
44
+ main_svc = services.find do |s|
45
+ name = s["name"].to_s.downcase
46
+ !%w[postgres postgresql mysql redis].any? { |db| name.include?(db) }
62
47
  end
63
- end
64
48
 
65
- # Mask sensitive variable values
66
- #
67
- # @param variables [Hash] Environment variables
68
- # @return [Hash] Variables with sensitive values masked
69
- def self.mask_variables(variables)
70
- variables.transform_values do |value|
71
- next value unless value.is_a?(String)
72
-
73
- # Check if variable name matches sensitive patterns
74
- is_sensitive = SENSITIVE_PATTERNS.any? { |pattern| value =~ pattern }
75
- is_sensitive ? '******' : value
49
+ db_svc = services.find do |s|
50
+ name = s["name"].to_s.downcase
51
+ %w[postgres postgresql mysql].any? { |db| name.include?(db) }
76
52
  end
53
+
54
+ {
55
+ success: true,
56
+ services: services,
57
+ main_service: main_svc,
58
+ db_service: db_svc
59
+ }
60
+ rescue JSON::ParserError => e
61
+ { success: false, error: "Failed to parse service list: #{e.message}", raw: out.to_s[0, 200] }
62
+ rescue => e
63
+ { success: false, error: "Unexpected error: #{e.message}" }
77
64
  end
78
65
  end
79
66
  end
@@ -1,137 +1,188 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "open3"
4
+
3
5
  module Clacky
4
6
  module DeployTools
5
- # Set environment variables for a Railway service
7
+ # Set environment variables on a Railway service via `railway variables --set`.
8
+ # Uses RAILWAY_TOKEN passed through environment — no clackycli wrapper needed.
9
+ #
10
+ # Supports both normal key=value pairs and Railway inter-service references
11
+ # like ${{postgres.DATABASE_PUBLIC_URL}} (pass raw_value: true to skip escaping).
6
12
  class SetDeployVariables
7
- PROTECTED_PREFIXES = ['CLACKY_'].freeze
8
-
13
+
9
14
  SENSITIVE_PATTERNS = [
10
- /password/i,
11
- /secret/i,
12
- /api_key/i,
13
- /token/i,
14
- /credential/i,
15
- /private_key/i
15
+ /password/i, /secret/i, /api_key/i,
16
+ /token/i, /credential/i, /private_key/i
16
17
  ].freeze
17
18
 
18
- # Execute the set_deploy_variables command
19
+ # Maximum number of variables to set in a single batch call
20
+ BATCH_SIZE = 20
21
+
22
+ # Retry config for transient failures
23
+ MAX_RETRIES = 3
24
+ RETRY_DELAY = 2 # seconds
25
+
26
+ # Set one or more environment variables on a Railway service.
27
+ # Batches all variables into a single `railway variables` call to minimize
28
+ # network connections and avoid SSL reset issues.
19
29
  #
20
- # @param service_name [String] Target service name
21
- # @param variables [Hash] Simple variables (KEY => VALUE)
22
- # @param ref_variables [Hash] Reference variables (KEY => SERVICE.VAR)
23
- # @return [Hash] Result of the operation
24
- def self.execute(service_name:, variables: {}, ref_variables: {})
25
- # Validate service name
26
- if service_name.nil? || service_name.empty?
27
- return {
28
- error: "Service name is required",
29
- details: "Please provide a valid service name"
30
- }
30
+ # @param service_name [String] Railway service name
31
+ # @param variables [Hash] KEY => VALUE pairs
32
+ # @param platform_token [String] RAILWAY_TOKEN for this deploy task
33
+ # @param raw_value [Boolean] when true, values are passed unquoted
34
+ # (for Railway ${{...}} references)
35
+ # @return [Hash] {
36
+ # success: Boolean,
37
+ # set_variables: Array<String>,
38
+ # errors: Array<Hash>
39
+ # }
40
+ def self.execute(service_name:, variables:, platform_token:, raw_value: false)
41
+ if service_name.nil? || service_name.strip.empty?
42
+ return { success: false, error: "service_name is required" }
31
43
  end
32
44
 
33
- results = {
34
- success: true,
35
- service: service_name,
36
- set_variables: [],
37
- skipped_variables: [],
38
- errors: []
39
- }
45
+ env = ENV.to_h.merge("RAILWAY_TOKEN" => platform_token)
40
46
 
41
- # Set simple variables
47
+ # Log all variables being set
42
48
  variables.each do |key, value|
43
- result = set_variable(service_name, key, value, is_reference: false)
44
- if result[:success]
45
- results[:set_variables] << { key: key, type: 'simple' }
46
- elsif result[:skipped]
47
- results[:skipped_variables] << { key: key, reason: result[:reason] }
48
- else
49
- results[:errors] << { key: key, error: result[:error] }
50
- end
49
+ log_value = sensitive?(key) ? "******" : value
50
+ puts " Setting #{key}=#{log_value}"
51
51
  end
52
52
 
53
- # Set reference variables
54
- ref_variables.each do |key, reference|
55
- result = set_variable(service_name, key, reference, is_reference: true)
53
+ # Split into batches to avoid command line length limits
54
+ set_vars = []
55
+ error_list = []
56
+ var_pairs = variables.map { |k, v| [k.to_s, v.to_s] }
57
+
58
+ var_pairs.each_slice(BATCH_SIZE) do |batch|
59
+ result = set_batch(env, service_name, batch, raw_value: raw_value)
56
60
  if result[:success]
57
- results[:set_variables] << { key: key, type: 'reference' }
58
- elsif result[:skipped]
59
- results[:skipped_variables] << { key: key, reason: result[:reason] }
61
+ set_vars.concat(batch.map(&:first))
60
62
  else
61
- results[:errors] << { key: key, error: result[:error] }
63
+ # Retry logic: attempt individual vars if batch fails
64
+ batch.each do |key, value|
65
+ individual = set_one_with_retry(env, service_name, key, value, raw_value: raw_value)
66
+ if individual[:success]
67
+ set_vars << key
68
+ else
69
+ error_list << { key: key, error: individual[:error] }
70
+ end
71
+ end
62
72
  end
63
73
  end
64
74
 
65
- # Overall success if no errors occurred
66
- results[:success] = results[:errors].empty?
67
- results
75
+ {
76
+ success: error_list.empty?,
77
+ set_variables: set_vars,
78
+ errors: error_list
79
+ }
68
80
  end
69
81
 
70
- # Set a single environment variable
82
+ # Set a batch of variables in a single railway command call.
71
83
  #
72
- # @param service_name [String] Service name
73
- # @param key [String] Variable name
74
- # @param value [String] Variable value or reference
75
- # @param is_reference [Boolean] Whether this is a reference variable
76
- # @return [Hash] Result of setting the variable
77
- def self.set_variable(service_name, key, value, is_reference:)
78
- # Skip protected variables
79
- if protected_variable?(key)
80
- return {
81
- success: false,
82
- skipped: true,
83
- reason: "Protected system variable (#{key})"
84
- }
84
+ # @param env [Hash] environment variables
85
+ # @param service_name [String] Railway service name
86
+ # @param pairs [Array<Array>] [[key, value], ...]
87
+ # @return [Hash] { success: true } or { success: false, error: String }
88
+ def self.set_batch(env, service_name, pairs, raw_value: false)
89
+ # Each --set argument is passed as a separate array element
90
+ set_flags = pairs.flat_map { |key, value| ["--set", "#{key}=#{value}"] }
91
+ cmd = ["railway", "variables", "--service", service_name, "--skip-deploys"] + set_flags
92
+
93
+ # Debug: print the full command being executed
94
+ puts " [DEBUG] Executing Railway CLI command:"
95
+ puts " [DEBUG] Array form: #{cmd.inspect}"
96
+ puts " [DEBUG] Shell form: #{cmd.join(' ')}"
97
+ puts " [DEBUG] with RAILWAY_TOKEN=#{env['RAILWAY_TOKEN']}" if env['RAILWAY_TOKEN']
98
+ $stdout.flush
99
+
100
+ # Use system() instead of Open3.capture3 to avoid stdin/stdout blocking issues
101
+ # system() inherits the current process's stdin/stdout/stderr directly
102
+ require 'timeout'
103
+
104
+ begin
105
+ success = Timeout.timeout(30) do
106
+ # Close stdin, suppress stdout, but keep stderr visible
107
+ system(env, *cmd, in: :close, out: File::NULL)
108
+ end
109
+ rescue Timeout::Error
110
+ return { success: false, error: "Railway CLI command timed out after 30 seconds" }
85
111
  end
86
112
 
87
- # Build the command
88
- if is_reference
89
- command = "clackycli variables -s #{shell_escape(service_name)} --set-ref #{shell_escape(key)}=#{shell_escape(value)}"
113
+ if success
114
+ { success: true }
90
115
  else
91
- command = "clackycli variables -s #{shell_escape(service_name)} --set #{shell_escape(key)}=#{shell_escape(value)}"
116
+ { success: false, error: "railway variables command failed (exit code: #{$?.exitstatus})" }
92
117
  end
118
+ end
93
119
 
94
- # Log (with sensitive masking)
95
- log_value = sensitive_variable?(key) ? '******' : value
96
- puts "Setting #{key}=#{log_value} on service #{service_name}"
120
+ # Set a single variable with retry logic for transient network errors.
121
+ #
122
+ # @return [Hash] { success: true } or { success: false, error: String }
123
+ def self.set_one_with_retry(env, service_name, key, value, raw_value: false)
124
+ last_error = nil
97
125
 
98
- # Execute command
99
- output = `#{command} 2>&1`
100
- exit_code = $?.exitstatus
126
+ MAX_RETRIES.times do |attempt|
127
+ result = set_one(env, service_name, key, value, raw_value: raw_value)
128
+ return result if result[:success]
101
129
 
102
- if exit_code == 0
103
- { success: true }
104
- else
105
- {
106
- success: false,
107
- skipped: false,
108
- error: output.strip
109
- }
130
+ last_error = result[:error]
131
+ # Only retry on connection/SSL errors
132
+ break unless last_error.to_s =~ /connection|ssl|reset|timeout|network/i
133
+
134
+ puts " ⚠️ Retrying #{key} (attempt #{attempt + 2}/#{MAX_RETRIES})..." if attempt < MAX_RETRIES - 1
135
+ sleep RETRY_DELAY
110
136
  end
137
+
138
+ { success: false, error: last_error }
111
139
  end
112
140
 
113
- # Check if a variable is protected
141
+ # Set a single variable. Builds the `railway variables --set` command.
114
142
  #
115
- # @param key [String] Variable name
116
- # @return [Boolean] True if protected
117
- def self.protected_variable?(key)
118
- PROTECTED_PREFIXES.any? { |prefix| key.start_with?(prefix) }
143
+ # @return [Hash] { success: true } or { success: false, error: String }
144
+ def self.set_one(env, service_name, key, value, raw_value: false)
145
+ assignment = "#{key}=#{value}"
146
+
147
+ cmd = [
148
+ "railway", "variables",
149
+ "--service", service_name,
150
+ "--skip-deploys",
151
+ "--set", assignment
152
+ ]
153
+
154
+ # Debug: print the full command being executed
155
+ puts " [DEBUG] Executing single var Railway CLI command:"
156
+ puts " [DEBUG] Array form: #{cmd.inspect}"
157
+ puts " [DEBUG] Shell form: #{cmd.join(' ')}"
158
+ puts " [DEBUG] with RAILWAY_TOKEN=#{env['RAILWAY_TOKEN']}" if env['RAILWAY_TOKEN']
159
+ $stdout.flush
160
+
161
+ # Use system() instead of Open3.capture3 to avoid stdin/stdout blocking issues
162
+ require 'timeout'
163
+
164
+ begin
165
+ success = Timeout.timeout(30) do
166
+ # Close stdin, suppress stdout, but keep stderr visible
167
+ system(env, *cmd, in: :close, out: File::NULL)
168
+ end
169
+ rescue Timeout::Error
170
+ return { success: false, error: "Railway CLI command timed out after 30 seconds" }
171
+ end
172
+
173
+ if success
174
+ { success: true }
175
+ else
176
+ { success: false, error: "railway variables command failed (exit code: #{$?.exitstatus})" }
177
+ end
119
178
  end
120
179
 
121
- # Check if a variable is sensitive
122
- #
123
- # @param key [String] Variable name
124
- # @return [Boolean] True if sensitive
125
- def self.sensitive_variable?(key)
126
- SENSITIVE_PATTERNS.any? { |pattern| key =~ pattern }
180
+ private_class_method def self.sensitive?(key)
181
+ SENSITIVE_PATTERNS.any? { |pat| key.match?(pat) }
127
182
  end
128
183
 
129
- # Escape shell arguments
130
- #
131
- # @param str [String] String to escape
132
- # @return [String] Escaped string
133
- def self.shell_escape(str)
134
- "'#{str.gsub("'", "'\\\\''")}'"
184
+ private_class_method def self.shell_escape(str)
185
+ "'#{str.to_s.gsub("'", "'\\\\''")}'"
135
186
  end
136
187
  end
137
188
  end