immosquare-cleaner 1.0.2 → 1.2.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/LICENSE +21 -0
- data/bin/immosquare-cleaner +23 -2
- data/lib/immosquare-cleaner/directory_cleaner.rb +1 -0
- data/lib/immosquare-cleaner/hook.rb +60 -0
- data/lib/immosquare-cleaner/markdown.rb +29 -2
- data/lib/immosquare-cleaner/processors/go.rb +31 -0
- data/lib/immosquare-cleaner/version.rb +1 -1
- data/lib/immosquare-cleaner.rb +3 -0
- data/linters/eslint.config.mjs +14 -0
- metadata +16 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c24a5c00bdd269b3943555309f236e4a854d0b6bef7f21956dd3f16d4a28e6b2
|
|
4
|
+
data.tar.gz: 692f7f8c8f1bed757e9b60e9d5f9e0a999258d730c77e3103f4d44a5e43ce253
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef79d787125d74355d3302375d313a2503e5c2ba6c2db67d46889dc9e7cb5d40a2fb0c242d8e1fa8a3240182d6b18c39ff4c68ccc24a0904f14e5fc46fc816c4
|
|
7
|
+
data.tar.gz: 73c13585ea4f87adb46b7a2712ac861e9c596b4f501e9027a7d8bd19be4f378fff498c17ff9c86e96fcd5332ada215cfa9b2fb1c01de81bce2a78b6c7b9fc382
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 immosquare
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/bin/immosquare-cleaner
CHANGED
|
@@ -31,16 +31,35 @@ OptionParser.new do |parser|
|
|
|
31
31
|
parser.on("-p", "--prevent-concurrent-write", "Prevent concurrent write") do
|
|
32
32
|
options[:prevent_concurrent_write] = true
|
|
33
33
|
end
|
|
34
|
+
parser.on("--hook", "Read the edited file from an AI agent hook payload on stdin") do
|
|
35
|
+
options[:hook] = true
|
|
36
|
+
end
|
|
34
37
|
parser.on("-h", "--help", "Prints this help") do
|
|
35
38
|
puts(parser)
|
|
36
39
|
exit
|
|
37
40
|
end
|
|
38
41
|
end.parse!
|
|
39
42
|
|
|
43
|
+
##============================================================##
|
|
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.
|
|
50
|
+
##============================================================##
|
|
51
|
+
if options[:hook]
|
|
52
|
+
hook_files = ImmosquareCleaner::Hook.file_paths($stdin.read)
|
|
53
|
+
exit(0) if hook_files.empty?
|
|
54
|
+
$stdout.reopen($stderr)
|
|
55
|
+
file_path = hook_files.first
|
|
56
|
+
else
|
|
57
|
+
file_path = ARGV[0]
|
|
58
|
+
end
|
|
59
|
+
|
|
40
60
|
##============================================================##
|
|
41
61
|
## Check if the file path is provided
|
|
42
62
|
##============================================================##
|
|
43
|
-
file_path = ARGV[0]
|
|
44
63
|
if !file_path
|
|
45
64
|
puts("Error: Please provide a file or directory path.")
|
|
46
65
|
exit(1)
|
|
@@ -78,7 +97,9 @@ end
|
|
|
78
97
|
## We can now call the clean method, passing the file absolute path and options
|
|
79
98
|
##============================================================##
|
|
80
99
|
file_path = File.expand_path(file_path)
|
|
81
|
-
if
|
|
100
|
+
if options[:hook]
|
|
101
|
+
hook_files.each {|path| ImmosquareCleaner.clean(path) }
|
|
102
|
+
elsif File.directory?(file_path)
|
|
82
103
|
##============================================================##
|
|
83
104
|
## Run from the cleaned directory: the project's
|
|
84
105
|
## config/initializers/immosquare-cleaner.rb (exclude_files) is
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module ImmosquareCleaner
|
|
4
|
+
##============================================================##
|
|
5
|
+
## `immosquare-cleaner --hook` : the files to clean come from the
|
|
6
|
+
## JSON an AI agent pipes on stdin after an edit, not from ARGV.
|
|
7
|
+
##
|
|
8
|
+
## Each agent describes the edit differently:
|
|
9
|
+
## - Claude Code, Gemini CLI : tool_input.file_path
|
|
10
|
+
## - Kimi Code : tool_input.path
|
|
11
|
+
## - Grok : toolInput.file_path
|
|
12
|
+
## - Cursor (afterFileEdit) : file_path at the top level
|
|
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`.
|
|
18
|
+
##
|
|
19
|
+
## A hook must never block the agent: anything that is not a
|
|
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.
|
|
23
|
+
##============================================================##
|
|
24
|
+
module Hook
|
|
25
|
+
PAYLOAD_KEYS = ["tool_input", "toolInput"].freeze
|
|
26
|
+
PATH_KEYS = ["file_path", "path", "target_file"].freeze
|
|
27
|
+
PATCH_LINE = /^\*\*\* (?:Add File|Update File|Move to): (.+)$/
|
|
28
|
+
|
|
29
|
+
def self.file_paths(raw_payload)
|
|
30
|
+
payload = JSON.parse(raw_payload.to_s)
|
|
31
|
+
return [] if !payload.is_a?(Hash)
|
|
32
|
+
|
|
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
|
|
37
|
+
|
|
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) }
|
|
41
|
+
rescue JSON::ParserError
|
|
42
|
+
[]
|
|
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)
|
|
59
|
+
end
|
|
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.
|
|
191
|
+
## header.
|
|
165
192
|
##============================================================##
|
|
166
|
-
cells = line
|
|
193
|
+
cells = table_cells(line)
|
|
167
194
|
|
|
168
195
|
##============================================================##
|
|
169
196
|
## We increase the size of the array if needed
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require "shellwords"
|
|
2
|
+
|
|
3
|
+
module ImmosquareCleaner
|
|
4
|
+
module Processors
|
|
5
|
+
class Go < Base
|
|
6
|
+
|
|
7
|
+
def self.match?(file_path)
|
|
8
|
+
file_path.end_with?(".go")
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run
|
|
12
|
+
if !system("which gofmt > /dev/null 2>&1")
|
|
13
|
+
warn("ERROR: gofmt is not installed. Please install Go with: brew install go")
|
|
14
|
+
return
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
##============================================================##
|
|
18
|
+
## gofmt flags:
|
|
19
|
+
## -s : apply gofmt's simplifications (redundant composite
|
|
20
|
+
## literal types, `s[a:len(s)]` → `s[a:]`...)
|
|
21
|
+
## -w : write the result back to the file in place
|
|
22
|
+
## Indentation stays in tabs: gofmt has no option for it, and
|
|
23
|
+
## converting to spaces would make every Go tool (gopls,
|
|
24
|
+
## `gofmt -l` in CI) flag the file as unformatted.
|
|
25
|
+
##============================================================##
|
|
26
|
+
launch_cmds(["gofmt -s -w #{Shellwords.escape(file_path)}"])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
data/lib/immosquare-cleaner.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "fileutils"
|
|
|
7
7
|
require_relative "immosquare-cleaner/configuration"
|
|
8
8
|
require_relative "immosquare-cleaner/markdown"
|
|
9
9
|
require_relative "immosquare-cleaner/directory_cleaner"
|
|
10
|
+
require_relative "immosquare-cleaner/hook"
|
|
10
11
|
|
|
11
12
|
##============================================================##
|
|
12
13
|
## Processors
|
|
@@ -19,6 +20,7 @@ require_relative "immosquare-cleaner/processors/json"
|
|
|
19
20
|
require_relative "immosquare-cleaner/processors/markdown"
|
|
20
21
|
require_relative "immosquare-cleaner/processors/shell"
|
|
21
22
|
require_relative "immosquare-cleaner/processors/rust"
|
|
23
|
+
require_relative "immosquare-cleaner/processors/go"
|
|
22
24
|
require_relative "immosquare-cleaner/processors/toml"
|
|
23
25
|
require_relative "immosquare-cleaner/processors/yaml"
|
|
24
26
|
require_relative "immosquare-cleaner/processors/prettier"
|
|
@@ -123,6 +125,7 @@ module ImmosquareCleaner
|
|
|
123
125
|
Processors::Markdown,
|
|
124
126
|
Processors::Shell,
|
|
125
127
|
Processors::Rust,
|
|
128
|
+
Processors::Go,
|
|
126
129
|
Processors::Toml
|
|
127
130
|
].freeze
|
|
128
131
|
|
data/linters/eslint.config.mjs
CHANGED
|
@@ -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.0
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- immosquare
|
|
@@ -149,9 +149,11 @@ dependencies:
|
|
|
149
149
|
- - "<="
|
|
150
150
|
- !ruby/object:Gem::Version
|
|
151
151
|
version: '1000.0'
|
|
152
|
-
description:
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
description: immosquare-cleaner formats Ruby, ERB, JS/TS, Rust, Go, TOML, Markdown,
|
|
153
|
+
Shell, JSON, YAML and CSS files with one consistent house style, delegating each
|
|
154
|
+
format to the right tool (RuboCop, erb_lint, ESLint, rustfmt, gofmt, taplo, shfmt,
|
|
155
|
+
Prettier). Run it on a file, a whole repository, on save, or as a Claude Code, Cursor
|
|
156
|
+
or Gemini CLI hook so every file an AI agent writes comes out formatted.
|
|
155
157
|
email:
|
|
156
158
|
- jules@immosquare.com
|
|
157
159
|
executables:
|
|
@@ -160,13 +162,16 @@ extensions: []
|
|
|
160
162
|
extra_rdoc_files: []
|
|
161
163
|
files:
|
|
162
164
|
- ".erb_linters"
|
|
165
|
+
- LICENSE
|
|
163
166
|
- bin/immosquare-cleaner
|
|
164
167
|
- lib/immosquare-cleaner.rb
|
|
165
168
|
- lib/immosquare-cleaner/configuration.rb
|
|
166
169
|
- lib/immosquare-cleaner/directory_cleaner.rb
|
|
170
|
+
- lib/immosquare-cleaner/hook.rb
|
|
167
171
|
- lib/immosquare-cleaner/markdown.rb
|
|
168
172
|
- lib/immosquare-cleaner/processors/base.rb
|
|
169
173
|
- lib/immosquare-cleaner/processors/erb.rb
|
|
174
|
+
- lib/immosquare-cleaner/processors/go.rb
|
|
170
175
|
- lib/immosquare-cleaner/processors/javascript.rb
|
|
171
176
|
- lib/immosquare-cleaner/processors/json.rb
|
|
172
177
|
- lib/immosquare-cleaner/processors/markdown.rb
|
|
@@ -201,10 +206,13 @@ files:
|
|
|
201
206
|
- linters/rubocop/cop/custom_cops/style/kwarg_priority_order.rb
|
|
202
207
|
- linters/rubocop/cop/style/method_call_with_args_parentheses_override.rb
|
|
203
208
|
- package.json
|
|
204
|
-
homepage: https://github.com/immosquare/
|
|
209
|
+
homepage: https://github.com/immosquare/immosquare-cleaner
|
|
205
210
|
licenses:
|
|
206
211
|
- MIT
|
|
207
|
-
metadata:
|
|
212
|
+
metadata:
|
|
213
|
+
source_code_uri: https://github.com/immosquare/immosquare-cleaner
|
|
214
|
+
changelog_uri: https://github.com/immosquare/immosquare-cleaner/blob/main/CHANGELOG.md
|
|
215
|
+
bug_tracker_uri: https://github.com/immosquare/immosquare-cleaner/issues
|
|
208
216
|
rdoc_options: []
|
|
209
217
|
require_paths:
|
|
210
218
|
- lib
|
|
@@ -222,5 +230,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
222
230
|
requirements: []
|
|
223
231
|
rubygems_version: 4.0.22
|
|
224
232
|
specification_version: 4
|
|
225
|
-
summary:
|
|
233
|
+
summary: One command to format every file of a repository, built to run as an AI agent
|
|
234
|
+
hook.
|
|
226
235
|
test_files: []
|