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 +4 -4
- data/lib/ask/tools/shell/edit.rb +82 -13
- data/lib/ask/tools/shell/file_mutation_queue.rb +84 -0
- data/lib/ask/tools/shell/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eea08734ef4ad1040ba2c9f2e7b615ae9931bb5e1d83dcb893e509487e603eaf
|
|
4
|
+
data.tar.gz: 47bda8b63b21e23fef543a5142658b363fd7758e01cd37921d4d880c0804d901
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75d972a45b9ecd3310c6693e0cd958745edb2af065dfdf366446bb7c4799d3359c3ff12d45dc58831eb3b59e88015a2221f0bec1c0857da6f755c2e1f32c7a4b
|
|
7
|
+
data.tar.gz: e5e8e83cb62a01e0ca737862917c559e9bade010f5104bb130f3ac8b44e7a615f1f6618e2ff30cde382caeb8cae186ed1da0c010f8f541bf884251667f29ab60
|
data/lib/ask/tools/shell/edit.rb
CHANGED
|
@@ -2,11 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
module Ask
|
|
4
4
|
module Tools
|
|
5
|
-
|
|
6
|
-
|
|
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 =
|
|
80
|
+
path = operations.expand_path(path)
|
|
20
81
|
|
|
21
|
-
unless
|
|
22
|
-
return Ask::Result.error(message: "File does not exist:
|
|
82
|
+
unless operations.file_exist?(path)
|
|
83
|
+
return Ask::Result.error(message: "File does not exist: " + path)
|
|
23
84
|
end
|
|
24
85
|
|
|
25
|
-
unless
|
|
26
|
-
return Ask::Result.error(message: "Not a file:
|
|
86
|
+
unless operations.file?(path)
|
|
87
|
+
return Ask::Result.error(message: "Not a file: " + path)
|
|
27
88
|
end
|
|
28
89
|
|
|
29
|
-
|
|
90
|
+
size = operations.file_size(path)
|
|
91
|
+
if size > MAX_FILE_SIZE
|
|
30
92
|
return Ask::Result.error(
|
|
31
|
-
message: "File too large (#{
|
|
93
|
+
message: "File too large (#{size} bytes). Maximum is #{MAX_FILE_SIZE} bytes."
|
|
32
94
|
)
|
|
33
95
|
end
|
|
34
96
|
|
|
35
|
-
|
|
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
|
-
|
|
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
|
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.
|
|
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
|