immosquare-cleaner 1.1.0 → 1.2.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: 684fefa6f6061c2cfd2a30c10852463e2a8a0605dc8d4078fe9cb8f136bd3f97
4
- data.tar.gz: 9437ae1b6de815f2b97d5dd10f61fb3ad412e6989969f73a10b481b9560efd8b
3
+ metadata.gz: ce05c959aa1bf3f812d6f676b2864037f59668a97da382b9bd1d7c7b13b6bde6
4
+ data.tar.gz: f6d6650dbbea335a474030d9808043820de93d25c6829f0423aaea1758a8d034
5
5
  SHA512:
6
- metadata.gz: d0962e5e3df0bf9493abeed5871e85fa8146787aec41b09ec37a322ccd88fd7d2454d1d08bb97aa03cf153f1e6ff75809b6d7f286fab0bf1f3ad5e4095f15291
7
- data.tar.gz: 53315a827d4a2030c1ce6e05bd70c24ded051f9259056fd695261c99e3c3d4e969c89543fb46498e5dfd39ff3a8ec076300352c41860491e020255f8b9edd83f
6
+ metadata.gz: 7d261eb4d38439c234c8836c5e04f56c3172277ea330722d352288e4b55ae2432d17e14ccb9db350ba6686d2016b9aeebe3c9eec98e61f0f7b85fe686ebf5a45
7
+ data.tar.gz: 6214f6c06cc2b1155a4272b12fd95d6a0531245b456eb904ac1c04b63a2e4b642745399e26004ddbbdff6301c0d0ba6257618bdc5d041fe00440bafbebe0d1f7
@@ -41,16 +41,18 @@ OptionParser.new do |parser|
41
41
  end.parse!
42
42
 
43
43
  ##============================================================##
44
- ## Hook mode : the edited file comes from the agent's JSON payload.
45
- ## Linters write their reports on stdout, which some agents
46
- ## (Gemini CLI) parse as the hook's JSON answer: everything goes
47
- ## to stderr instead. An unsupported or missing file ends the
48
- ## hook silently with exit 0, so the agent is never blocked.
44
+ ## Hook mode : the edited files come from the agent's JSON payload
45
+ ## (one file, or several for a Codex patch). Linters write their
46
+ ## reports on stdout, which some agents (Gemini CLI, Codex) parse
47
+ ## as the hook's JSON answer: everything goes to stderr instead.
48
+ ## No supported file ends the hook silently with exit 0, so the
49
+ ## agent is never blocked.
49
50
  ##============================================================##
50
51
  if options[:hook]
51
- file_path = ImmosquareCleaner::Hook.file_path($stdin.read)
52
- exit(0) if file_path.nil?
52
+ hook_files = ImmosquareCleaner::Hook.file_paths($stdin.read)
53
+ exit(0) if hook_files.empty?
53
54
  $stdout.reopen($stderr)
55
+ file_path = hook_files.first
54
56
  else
55
57
  file_path = ARGV[0]
56
58
  end
@@ -95,7 +97,9 @@ end
95
97
  ## We can now call the clean method, passing the file absolute path and options
96
98
  ##============================================================##
97
99
  file_path = File.expand_path(file_path)
98
- if File.directory?(file_path)
100
+ if options[:hook]
101
+ hook_files.each {|path| ImmosquareCleaner.clean(path) }
102
+ elsif File.directory?(file_path)
99
103
  ##============================================================##
100
104
  ## Run from the cleaned directory: the project's
101
105
  ## config/initializers/immosquare-cleaner.rb (exclude_files) is
@@ -27,7 +27,10 @@ module ImmosquareCleaner
27
27
  ## EXCLUDED_PATHS : paths relative to the root — generated or
28
28
  ## vendored content that repositories commit
29
29
  ## (db/schema.rb, Tauri schemas...) and test
30
- ## fixtures that must stay as written
30
+ ## fixtures that must stay as written: YAML
31
+ ## fixtures hold ERB tags Prettier would break,
32
+ ## sample files are compared byte for byte.
33
+ ## Test code itself is cleaned like any source.
31
34
  ##============================================================##
32
35
  EXCLUDED_DIRS = [
33
36
  ".git",
@@ -47,9 +50,10 @@ module ImmosquareCleaner
47
50
  "app/assets/images",
48
51
  "db",
49
52
  "public",
53
+ "spec/fixtures",
50
54
  "src-tauri/gen",
51
55
  "src-tauri/permissions/autogenerated",
52
- "test"
56
+ "test/fixtures"
53
57
  ].freeze
54
58
  EXCLUDED_FILES = [
55
59
  "npm-shrinkwrap.json",
@@ -2,36 +2,59 @@ require "json"
2
2
 
3
3
  module ImmosquareCleaner
4
4
  ##============================================================##
5
- ## `immosquare-cleaner --hook` : the file to clean comes from the
5
+ ## `immosquare-cleaner --hook` : the files to clean come from the
6
6
  ## JSON an AI agent pipes on stdin after an edit, not from ARGV.
7
7
  ##
8
- ## Each agent nests the edited path differently:
8
+ ## Each agent describes the edit differently:
9
9
  ## - Claude Code, Gemini CLI : tool_input.file_path
10
10
  ## - Kimi Code : tool_input.path
11
11
  ## - Grok : toolInput.file_path
12
12
  ## - Cursor (afterFileEdit) : file_path at the top level
13
- ## A relative path is resolved against the payload's `cwd`.
13
+ ## - Codex (apply_patch) : tool_input.command holds the patch,
14
+ ## one `*** Update File: <path>` (or
15
+ ## Add File / Move to) line per file
16
+ ## Relative paths are resolved against tool_input.workdir (Codex),
17
+ ## then the payload's `cwd`.
14
18
  ##
15
19
  ## A hook must never block the agent: anything that is not a
16
- ## supported file (no path, deleted file, directory, LICENSE,
17
- ## .env...) is skipped instead of reported.
20
+ ## supported file (deleted file, directory, symlink, LICENSE,
21
+ ## .env...) is dropped instead of reported. A symlink is never
22
+ ## followed, so an edit cannot reformat a file outside the project.
18
23
  ##============================================================##
19
24
  module Hook
20
25
  PAYLOAD_KEYS = ["tool_input", "toolInput"].freeze
21
26
  PATH_KEYS = ["file_path", "path", "target_file"].freeze
27
+ PATCH_LINE = /^\*\*\* (?:Add File|Update File|Move to): (.+)$/
22
28
 
23
- def self.file_path(raw_payload)
29
+ def self.file_paths(raw_payload)
24
30
  payload = JSON.parse(raw_payload.to_s)
25
- return if !payload.is_a?(Hash)
31
+ return [] if !payload.is_a?(Hash)
26
32
 
27
- sources = PAYLOAD_KEYS.map {|key| payload[key] }.grep(Hash) + [payload]
28
- path = sources.lazy.flat_map {|source| PATH_KEYS.map {|key| source[key] } }.find {|value| value.is_a?(String) && !value.empty? }
29
- return if path.nil?
33
+ sources = PAYLOAD_KEYS.map {|key| payload[key] }.grep(Hash)
34
+ cwd = payload["cwd"].is_a?(String) && !payload["cwd"].empty? ? payload["cwd"] : Dir.pwd
35
+ workdir = sources.map {|source| source["workdir"] }.find {|value| value.is_a?(String) && !value.empty? }
36
+ base_dir = workdir ? File.expand_path(workdir, cwd) : cwd
30
37
 
31
- path = File.expand_path(path, payload["cwd"].is_a?(String) ? payload["cwd"] : Dir.pwd)
32
- path if File.file?(path) && ImmosquareCleaner.supported?(path)
38
+ paths = patch_paths(sources)
39
+ paths = [direct_path(sources + [payload])].compact if paths.empty?
40
+ paths.map {|path| File.expand_path(path, base_dir) }.uniq.select {|path| File.file?(path) && !File.symlink?(path) && ImmosquareCleaner.supported?(path) }
33
41
  rescue JSON::ParserError
34
- nil
42
+ []
35
43
  end
44
+
45
+ def self.direct_path(sources)
46
+ sources.lazy.flat_map {|source| PATH_KEYS.map {|key| source[key] } }.find {|value| value.is_a?(String) && !value.empty? }
47
+ end
48
+
49
+ ##============================================================##
50
+ ## Codex sends the patch as a string, older versions as an argv
51
+ ## array (`["apply_patch", "*** Begin Patch..."]`)
52
+ ##============================================================##
53
+ def self.patch_paths(sources)
54
+ patch = sources.map {|source| source["command"] }.find {|value| value.is_a?(String) || value.is_a?(Array) }
55
+ Array(patch).grep(String).join("\n").scan(PATCH_LINE).flatten.map(&:strip)
56
+ end
57
+
58
+ private_class_method(:direct_path, :patch_paths)
36
59
  end
37
60
  end
@@ -144,6 +144,33 @@ module ImmosquareCleaner
144
144
  nil
145
145
  end
146
146
 
147
+ ##============================================================##
148
+ ## Splits a table row on its delimiting pipes. A pipe escaped as
149
+ ## `\|` is cell content (GFM), not a delimiter: splitting on it
150
+ ## would add a column to the whole table. A trailing empty field
151
+ ## only exists when the row ends with a delimiting pipe, which
152
+ ## closes the row rather than opening an extra cell.
153
+ ##============================================================##
154
+ def table_cells(line)
155
+ cells = [+""]
156
+ escaped = false
157
+ line.strip.delete_prefix("|").each_char do |char|
158
+ if escaped
159
+ cells.last << char
160
+ escaped = false
161
+ elsif char == "\\"
162
+ cells.last << char
163
+ escaped = true
164
+ elsif char == "|"
165
+ cells << +""
166
+ else
167
+ cells.last << char
168
+ end
169
+ end
170
+ cells.pop if cells.size > 1 && cells.last.empty?
171
+ cells.map(&:strip)
172
+ end
173
+
147
174
  ##============================================================##
148
175
  ## we want to clean the markdown files to have a uniform style
149
176
  ## for the tables.
@@ -161,9 +188,9 @@ module ImmosquareCleaner
161
188
  ## above a column of row labels, a value that does not apply).
162
189
  ## Rejecting empty cells would shift every following cell one
163
190
  ## column to the left and silently file values under the wrong
164
- ## header. `split("|", -1)` keeps trailing empty fields.
191
+ ## header.
165
192
  ##============================================================##
166
- cells = line.strip.delete_prefix("|").delete_suffix("|").split("|", -1).map(&:strip)
193
+ cells = table_cells(line)
167
194
 
168
195
  ##============================================================##
169
196
  ## We increase the size of the array if needed
@@ -1,3 +1,3 @@
1
1
  module ImmosquareCleaner
2
- VERSION = "1.1.0".freeze
2
+ VERSION = "1.2.1".freeze
3
3
  end
@@ -134,6 +134,20 @@ export default [
134
134
  "no-undef": 0
135
135
  }
136
136
  },
137
+ //============================================================//
138
+ // JSX files: ESLint only lints .js/.mjs/.cjs by default, so a
139
+ // .jsx file matched by no `files` pattern is skipped with
140
+ // "File ignored because no matching configuration was supplied".
141
+ // The common rules above carry no `files` key and apply here too.
142
+ //============================================================//
143
+ {
144
+ files: ["**/*.jsx"],
145
+ languageOptions: {
146
+ parserOptions: {
147
+ ecmaFeatures: { jsx: true }
148
+ }
149
+ }
150
+ },
137
151
  erb.configs.recommended,
138
152
  {
139
153
  linterOptions: {
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: immosquare-cleaner
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - immosquare