ask-tools-shell 0.3.0 → 0.3.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eea08734ef4ad1040ba2c9f2e7b615ae9931bb5e1d83dcb893e509487e603eaf
4
- data.tar.gz: 47bda8b63b21e23fef543a5142658b363fd7758e01cd37921d4d880c0804d901
3
+ metadata.gz: 139a762b4412fcf6dac23e7360f7f49e537f32272491865e90f3ec36ae27da44
4
+ data.tar.gz: d074b1a2173259a65fc9f9eab1efd3424f2ef4bfcea2e3e868a3ee281ca5d5db
5
5
  SHA512:
6
- metadata.gz: 75d972a45b9ecd3310c6693e0cd958745edb2af065dfdf366446bb7c4799d3359c3ff12d45dc58831eb3b59e88015a2221f0bec1c0857da6f755c2e1f32c7a4b
7
- data.tar.gz: e5e8e83cb62a01e0ca737862917c559e9bade010f5104bb130f3ac8b44e7a615f1f6618e2ff30cde382caeb8cae186ed1da0c010f8f541bf884251667f29ab60
6
+ metadata.gz: 8e16c060e637dc9dfa1c420fff9eaf5c357a8abc2ce7c9489fa082bd2f319b65e4b03b64677d3310df359725877e313c334c0a39a5eaa0b5bd8ff44253c2aac9
7
+ data.tar.gz: ab4bbada08684a89410144614778b70be6a5bc7e7e1d0dbd596aac2727df3eab80d3ec94de9cf172d4a0519d6a1494d102010087c61ea9bee6118fed549bf17f
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0 (2026-06-21)
4
+
5
+ - Added `EditOperations` pluggable interface (read_file, write_file, file_exist?, file?, file_size, expand_path)
6
+ - Added `DefaultEditOperations` default implementation
7
+ - Added BOM detection/stripping to Edit tool (`Shell.strip_bom`)
8
+ - Added line ending detection and preservation to Edit tool (`Shell.detect_line_ending`, `Shell.normalize_line_endings`, `Shell.restore_line_endings`)
9
+ - Added `FileMutationQueue` for atomic batch file edits with rollback
10
+
11
+ ## 0.2.2
12
+
13
+ - Various fixes
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Ask
6
+ module Tools
7
+ module Shell
8
+ module ApplyPatchOperations
9
+ def read_file(path)
10
+ File.read(path)
11
+ end
12
+
13
+ def write_file(path, content)
14
+ File.write(path, content)
15
+ end
16
+
17
+ def file_exist?(path)
18
+ File.exist?(path)
19
+ end
20
+
21
+ def file?(path)
22
+ File.file?(path)
23
+ end
24
+
25
+ def delete_file(path)
26
+ File.delete(path)
27
+ end
28
+
29
+ def expand_path(path)
30
+ File.expand_path(path)
31
+ end
32
+
33
+ def mkdir_p(path)
34
+ FileUtils.mkdir_p(File.dirname(path))
35
+ end
36
+ end
37
+
38
+ class DefaultApplyPatchOperations
39
+ include ApplyPatchOperations
40
+ end
41
+ end
42
+
43
+ # Apply a patch to files using a unified diff format.
44
+ # Supports creating, updating, and deleting files within a
45
+ # "*** Begin Patch" / "*** End Patch" envelope.
46
+ class ApplyPatch < Ask::Tool
47
+ description "Edit files using a unified diff format. " \
48
+ "Wrap all changes in a \"*** Begin Patch\" / \"*** End Patch\" envelope. " \
49
+ "Each file section starts with a header: " \
50
+ "\"*** Add File: <path>\" for new files, " \
51
+ "\"*** Update File: <path>\" for changes, or " \
52
+ "\"*** Delete File: <path>\" for removals. " \
53
+ "Prefix new lines with +."
54
+
55
+ param :patchText, type: :string, desc: "The full patch text describing all file changes", required: true
56
+
57
+ attr_writer :operations
58
+
59
+ def operations
60
+ @operations ||= Shell::DefaultApplyPatchOperations.new
61
+ end
62
+
63
+ MAX_FILE_SIZE = 10_000_000
64
+
65
+ def execute(patchText:)
66
+ patches = parse_patch(patchText)
67
+ return Ask::Result.error(message: "No valid patch sections found") if patches.empty?
68
+
69
+ results = []
70
+ patches.each do |entry|
71
+ path = operations.expand_path(entry[:path])
72
+
73
+ case entry[:type]
74
+ when :add
75
+ if operations.file_exist?(path)
76
+ return Ask::Result.error(message: "File already exists: #{path}")
77
+ end
78
+ operations.mkdir_p(path)
79
+ content = entry[:lines].join("\n")
80
+ content += "\n" unless content.end_with?("\n")
81
+ operations.write_file(path, content)
82
+ results << { action: "add", path: entry[:path], lines: entry[:lines].size }
83
+
84
+ when :update
85
+ unless operations.file_exist?(path)
86
+ return Ask::Result.error(message: "File does not exist: #{path}")
87
+ end
88
+ unless operations.file?(path)
89
+ return Ask::Result.error(message: "Not a file: #{path}")
90
+ end
91
+
92
+ raw = operations.read_file(path)
93
+ size = raw.bytesize
94
+ if size > MAX_FILE_SIZE
95
+ return Ask::Result.error(message: "File too large (#{size} bytes)")
96
+ end
97
+
98
+ new_content = apply_chunks(raw, entry[:chunks])
99
+ operations.write_file(path, new_content)
100
+ results << { action: "update", path: entry[:path] }
101
+
102
+ when :delete
103
+ unless operations.file_exist?(path)
104
+ return Ask::Result.error(message: "File does not exist: #{path}")
105
+ end
106
+ operations.delete_file(path)
107
+ results << { action: "delete", path: entry[:path] }
108
+ end
109
+ end
110
+
111
+ summary = results.map { |r| "#{r[:action].upcase_first} #{r[:path]}" }.join("\n")
112
+ Ask::Result.ok(data: { summary: summary, results: results }, metadata: { results: results })
113
+ end
114
+
115
+ private
116
+
117
+ PATCH_START = "*** Begin Patch"
118
+ PATCH_END = "*** End Patch"
119
+ ADD_HEADER = /^\*\*\* Add File:\s+(.+)/i
120
+ UPDATE_HEADER = /^\*\*\* Update File:\s+(.+)/i
121
+ DELETE_HEADER = /^\*\*\* Delete File:\s+(.+)/i
122
+ HUNK_HEADER = /^@@/
123
+
124
+ def parse_patch(text)
125
+ text = text.dup.force_encoding("UTF-8")
126
+ start_idx = text.index(PATCH_START)
127
+ return [] unless start_idx
128
+
129
+ after_start = text[(start_idx + PATCH_START.length)..]
130
+ end_idx = after_start.index(PATCH_END)
131
+ body = end_idx ? after_start[0...end_idx] : after_start
132
+
133
+ entries = []
134
+ lines = body.split("\n")
135
+ i = 0
136
+ while i < lines.length
137
+ line = lines[i]
138
+
139
+ if (m = line.match(ADD_HEADER))
140
+ path = m[1].strip
141
+ i += 1
142
+ content_lines = []
143
+ while i < lines.length && !lines[i].start_with?("***")
144
+ content_lines << lines[i].sub(/^\+/, "") if lines[i].start_with?("+")
145
+ i += 1
146
+ end
147
+ entries << { type: :add, path: path, lines: content_lines }
148
+
149
+ elsif (m = line.match(UPDATE_HEADER))
150
+ path = m[1].strip
151
+ i += 1
152
+ chunks = []
153
+ while i < lines.length && !lines[i].start_with?("***")
154
+ if lines[i].match?(HUNK_HEADER)
155
+ i += 1
156
+ old_lines = []
157
+ new_lines = []
158
+ while i < lines.length && !lines[i].start_with?("***") && !lines[i].match?(HUNK_HEADER) && !lines[i].start_with?("***")
159
+ if lines[i].start_with?("-")
160
+ old_lines << lines[i][1..]
161
+ elsif lines[i].start_with?("+")
162
+ new_lines << lines[i][1..]
163
+ elsif lines[i].start_with?(" ")
164
+ old_lines << lines[i][1..]
165
+ new_lines << lines[i][1..]
166
+ end
167
+ i += 1
168
+ end
169
+ chunks << { old: old_lines, new: new_lines } unless old_lines.empty? && new_lines.empty?
170
+ else
171
+ i += 1
172
+ end
173
+ end
174
+ entries << { type: :update, path: path, chunks: chunks }
175
+
176
+ elsif (m = line.match(DELETE_HEADER))
177
+ entries << { type: :delete, path: m[1].strip }
178
+ i += 1
179
+
180
+ else
181
+ i += 1
182
+ end
183
+ end
184
+
185
+ entries
186
+ end
187
+
188
+ def apply_chunks(content, chunks)
189
+ return content if chunks.empty?
190
+
191
+ lines = content.split("\n", -1)
192
+ chunks.each do |chunk|
193
+ old_text = chunk[:old].join("\n")
194
+ new_text = chunk[:new].join("\n")
195
+ file_text = lines.join("\n")
196
+ idx = file_text.index(old_text)
197
+ next unless idx
198
+
199
+ before = file_text[0...idx]
200
+ after = file_text[(idx + old_text.length)..]
201
+ file_text = before + new_text + after
202
+ lines = file_text.split("\n", -1)
203
+ end
204
+ lines.join("\n")
205
+ end
206
+ end
207
+ end
208
+ end
@@ -42,7 +42,7 @@ module Ask
42
42
 
43
43
  # Detect the dominant line ending in content.
44
44
  def self.detect_line_ending(content)
45
- crlf = content.count("\r\n")
45
+ crlf = content.scan("\r\n").size
46
46
  lf = content.count("\n") - crlf
47
47
  crlf > lf ? "\r\n" : "\n"
48
48
  end
@@ -3,7 +3,7 @@
3
3
  module Ask
4
4
  module Tools
5
5
  module Shell
6
- VERSION = "0.3.0"
6
+ VERSION = "0.3.1"
7
7
  end
8
8
  end
9
9
  end
@@ -9,11 +9,16 @@ require_relative "shell/edit"
9
9
  require_relative "shell/glob"
10
10
  require_relative "shell/grep"
11
11
  require_relative "shell/code"
12
+ require_relative "shell/apply_patch"
12
13
 
13
14
  module Ask
14
15
  module Tools
15
16
  module Shell
16
- TOOLS = [Bash, Read, Write, Edit, Glob, Grep, Code].freeze
17
+ TOOLS = [Bash, Read, Write, Edit, Glob, Grep, Code, ApplyPatch].freeze
18
+
19
+ def self.all
20
+ TOOLS.map(&:new)
21
+ end
17
22
  end
18
23
  end
19
24
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-tools-shell
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -13,28 +13,28 @@ dependencies:
13
13
  name: ask-tools
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
18
  version: '0.1'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0.1'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: ask-sandbox-providers
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: '0.1'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
39
  version: '0.1'
40
40
  - !ruby/object:Gem::Dependency
@@ -86,11 +86,13 @@ executables: []
86
86
  extensions: []
87
87
  extra_rdoc_files: []
88
88
  files:
89
+ - CHANGELOG.md
89
90
  - LICENSE
90
91
  - README.md
91
92
  - lib/ask-tools-shell.rb
92
93
  - lib/ask/skills/shell.patterns/SKILL.md
93
94
  - lib/ask/tools/shell.rb
95
+ - lib/ask/tools/shell/apply_patch.rb
94
96
  - lib/ask/tools/shell/bash.rb
95
97
  - lib/ask/tools/shell/code.rb
96
98
  - lib/ask/tools/shell/edit.rb
@@ -103,7 +105,10 @@ files:
103
105
  homepage: https://github.com/ask-rb/ask-tools-shell
104
106
  licenses:
105
107
  - MIT
106
- metadata: {}
108
+ metadata:
109
+ homepage_uri: https://github.com/ask-rb/ask-tools-shell
110
+ source_code_uri: https://github.com/ask-rb/ask-tools-shell
111
+ changelog_uri: https://github.com/ask-rb/ask-tools-shell/blob/master/CHANGELOG.md
107
112
  rdoc_options: []
108
113
  require_paths:
109
114
  - lib