ask-tools-shell 0.2.2 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03ed1e1f856f94afa4d5f5dc4b8fb278ad2598ddc6b7d698606c035a9e4034a9
4
- data.tar.gz: a040e94935e8a1a5e03530f84e225ee6f55e662fc1d2d1ad3840bf4bcc6e168d
3
+ metadata.gz: eea08734ef4ad1040ba2c9f2e7b615ae9931bb5e1d83dcb893e509487e603eaf
4
+ data.tar.gz: 47bda8b63b21e23fef543a5142658b363fd7758e01cd37921d4d880c0804d901
5
5
  SHA512:
6
- metadata.gz: d8718eb919c2eec978e61c081a036cfbf34cb9adcc829bc0b4637fe490a566f9e573de48a6e327064558868b6abf351f9373178700e5d662b80f6cb128ad788c
7
- data.tar.gz: 95312eb2e6b6db1dafca32d8bf074a9d789c4a3cca13c30ed16e116a03e726f308250f08d9292fb7f7f05bed829cd7e11bb792370376e86bb46f938a1b42513c
6
+ metadata.gz: 75d972a45b9ecd3310c6693e0cd958745edb2af065dfdf366446bb7c4799d3359c3ff12d45dc58831eb3b59e88015a2221f0bec1c0857da6f755c2e1f32c7a4b
7
+ data.tar.gz: e5e8e83cb62a01e0ca737862917c559e9bade010f5104bb130f3ac8b44e7a615f1f6618e2ff30cde382caeb8cae186ed1da0c010f8f541bf884251667f29ab60
@@ -2,11 +2,66 @@
2
2
 
3
3
  module Ask
4
4
  module Tools
5
- # Edit a file by replacing exact text.
6
- # Uses exact string matching provide enough surrounding context for uniqueness.
5
+ module Shell
6
+ # Pluggable operations for the Edit tool.
7
+ module EditOperations
8
+ def read_file(path)
9
+ File.read(path)
10
+ end
11
+
12
+ def write_file(path, content)
13
+ File.write(path, content)
14
+ end
15
+
16
+ def file_exist?(path)
17
+ File.exist?(path)
18
+ end
19
+
20
+ def file?(path)
21
+ File.file?(path)
22
+ end
23
+
24
+ def file_size(path)
25
+ File.size(path)
26
+ end
27
+
28
+ def expand_path(path)
29
+ File.expand_path(path)
30
+ end
31
+ end
32
+
33
+ class DefaultEditOperations
34
+ include EditOperations
35
+ end
36
+
37
+ # Strip UTF-8 BOM from content. Returns { bom: String, text: String }.
38
+ def self.strip_bom(content)
39
+ bom = content.byteslice(0, 3) == "\xEF\xBB\xBF" ? content.byteslice(0, 3) : ""
40
+ [bom, bom.empty? ? content : content.byteslice(3..)]
41
+ end
42
+
43
+ # Detect the dominant line ending in content.
44
+ def self.detect_line_ending(content)
45
+ crlf = content.count("\r\n")
46
+ lf = content.count("\n") - crlf
47
+ crlf > lf ? "\r\n" : "\n"
48
+ end
49
+
50
+ # Normalize line endings to LF.
51
+ def self.normalize_line_endings(content)
52
+ content.gsub("\r\n", "\n").gsub("\r", "\n")
53
+ end
54
+
55
+ # Restore original line endings after normalization.
56
+ def self.restore_line_endings(content, original_ending)
57
+ return content if original_ending == "\n"
58
+ content.gsub("\n", original_ending)
59
+ end
60
+ end
61
+
62
+ # Edit a file by replacing exact text. Supports BOM, line-ending preservation.
7
63
  class Edit < Ask::Tool
8
- description "Edit a file by replacing exact text. " \
9
- "Uses exact string matching — provide enough surrounding context for uniqueness."
64
+ description "Edit a file by replacing exact text."
10
65
 
11
66
  param :path, type: :string, desc: "Absolute path to the file", required: true
12
67
  param :old_string, type: :string, desc: "Exact text to replace", required: true
@@ -15,24 +70,34 @@ module Ask
15
70
 
16
71
  MAX_FILE_SIZE = 1_000_000
17
72
 
73
+ attr_writer :operations
74
+
75
+ def operations
76
+ @operations ||= Shell::DefaultEditOperations.new
77
+ end
78
+
18
79
  def execute(path:, old_string:, new_string:, replace_all: false)
19
- path = File.expand_path(path)
80
+ path = operations.expand_path(path)
20
81
 
21
- unless File.exist?(path)
22
- return Ask::Result.error(message: "File does not exist: #{path}")
82
+ unless operations.file_exist?(path)
83
+ return Ask::Result.error(message: "File does not exist: " + path)
23
84
  end
24
85
 
25
- unless File.file?(path)
26
- return Ask::Result.error(message: "Not a file: #{path}")
86
+ unless operations.file?(path)
87
+ return Ask::Result.error(message: "Not a file: " + path)
27
88
  end
28
89
 
29
- if File.size(path) > MAX_FILE_SIZE
90
+ size = operations.file_size(path)
91
+ if size > MAX_FILE_SIZE
30
92
  return Ask::Result.error(
31
- message: "File too large (#{File.size(path)} bytes). Maximum is #{MAX_FILE_SIZE} bytes."
93
+ message: "File too large (#{size} bytes). Maximum is #{MAX_FILE_SIZE} bytes."
32
94
  )
33
95
  end
34
96
 
35
- content = File.read(path)
97
+ raw = operations.read_file(path)
98
+ bom, content = Shell.strip_bom(raw)
99
+ original_ending = Shell.detect_line_ending(content)
100
+ content = Shell.normalize_line_endings(content)
36
101
 
37
102
  if replace_all
38
103
  count = content.scan(old_string).size
@@ -45,7 +110,11 @@ module Ask
45
110
  content = content.sub(old_string, new_string)
46
111
  end
47
112
 
48
- File.write(path, content)
113
+ # Restore line endings and re-add BOM
114
+ content = Shell.restore_line_endings(content, original_ending)
115
+ content = bom + content
116
+
117
+ operations.write_file(path, content)
49
118
  Ask::Result.ok(data: { path: path, replacements: count },
50
119
  metadata: { path: path, replacements: count })
51
120
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module Tools
5
+ module Shell
6
+ # Queues file mutations and applies them atomically.
7
+ # Rollback on any failure — no partial writes.
8
+ #
9
+ # queue = FileMutationQueue.new
10
+ # queue.stage("path/to/file.rb", ->(content) {
11
+ # content.gsub("old", "new")
12
+ # })
13
+ # queue.apply! # reads all staged files, applies transforms, writes back
14
+ #
15
+ class FileMutationQueue
16
+ class ApplyError < StandardError; end
17
+
18
+ def initialize(operations: nil)
19
+ @staged = []
20
+ @operations = operations || DefaultEditOperations.new
21
+ end
22
+
23
+ # Stage a mutation for a file.
24
+ # @param path [String] absolute path to the file
25
+ # @yield [content] raw file content, returns modified content
26
+ # @yieldparam content [String] the original file content
27
+ # @yieldreturn [String] the modified content
28
+ def stage(path, &block)
29
+ @staged << { path: File.expand_path(path), block: block }
30
+ end
31
+
32
+ # Apply all staged mutations atomically.
33
+ # Reads all files, applies transforms, then writes all back.
34
+ # If any write fails, all written files are rolled back to original.
35
+ # @return [Array<Hash>] results with :path, :original_size, :new_size, :success
36
+ # @raise [ApplyError] if any mutation fails (after rollback)
37
+ def apply!
38
+ snapshots = []
39
+
40
+ # Phase 1: read all files and apply transforms
41
+ @staged.each do |entry|
42
+ path = entry[:path]
43
+ original = @operations.read_file(path)
44
+ modified = entry[:block].call(original)
45
+ snapshots << { path: path, original: original, modified: modified }
46
+ end
47
+
48
+ # Phase 2: write all files, track rollback info
49
+ written = []
50
+ begin
51
+ snapshots.each do |s|
52
+ @operations.write_file(s[:path], s[:modified])
53
+ written << s[:path]
54
+ end
55
+ rescue => e
56
+ # Rollback: restore originals for files we already wrote
57
+ written.each do |path|
58
+ snapshot = snapshots.find { |s| s[:path] == path }
59
+ @operations.write_file(path, snapshot[:original]) if snapshot
60
+ rescue => rb_e
61
+ $stderr.puts "[FileMutationQueue] Rollback failed for #{path}: #{rb_e.message}"
62
+ end
63
+ raise ApplyError, "Mutation failed at #{e.class}: #{e.message}. Rolled back #{written.size} files."
64
+ end
65
+
66
+ snapshots.map do |s|
67
+ { path: s[:path], original_size: s[:original].length,
68
+ new_size: s[:modified].length, success: true }
69
+ end
70
+ end
71
+
72
+ # Clear all staged mutations without applying.
73
+ def clear!
74
+ @staged.clear
75
+ end
76
+
77
+ # Number of staged mutations.
78
+ def size
79
+ @staged.size
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -3,7 +3,7 @@
3
3
  module Ask
4
4
  module Tools
5
5
  module Shell
6
- VERSION = "0.2.2"
6
+ VERSION = "0.3.0"
7
7
  end
8
8
  end
9
9
  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.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -94,6 +94,7 @@ files:
94
94
  - lib/ask/tools/shell/bash.rb
95
95
  - lib/ask/tools/shell/code.rb
96
96
  - lib/ask/tools/shell/edit.rb
97
+ - lib/ask/tools/shell/file_mutation_queue.rb
97
98
  - lib/ask/tools/shell/glob.rb
98
99
  - lib/ask/tools/shell/grep.rb
99
100
  - lib/ask/tools/shell/read.rb