hank 0.1.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 +7 -0
- data/.clodignore +32 -0
- data/.rubocop.yml +59 -0
- data/CLAUDE.md +22 -0
- data/Gemfile +11 -0
- data/LICENSE +21 -0
- data/README.md +70 -0
- data/Rakefile +17 -0
- data/bin/hank +7 -0
- data/hank.gemspec +55 -0
- data/lib/hank/cli.rb +162 -0
- data/lib/hank/file_selector.rb +204 -0
- data/lib/hank/hankfile.rb +70 -0
- data/lib/hank/path_utils.rb +47 -0
- data/lib/hank/symlink_manager.rb +94 -0
- data/lib/hank/validator.rb +65 -0
- data/lib/hank/version.rb +6 -0
- data/lib/hank.rb +25 -0
- data/sorbet/config +5 -0
- data/sorbet/rbi/annotations/.gitattributes +1 -0
- data/sorbet/rbi/annotations/colorize.rbi +121 -0
- data/sorbet/rbi/annotations/minitest.rbi +119 -0
- data/sorbet/rbi/annotations/rainbow.rbi +269 -0
- data/sorbet/rbi/gems/.gitattributes +1 -0
- data/sorbet/rbi/gems/ansi.rbi +398 -0
- data/sorbet/rbi/gems/ansi@1.5.0.rbi +688 -0
- data/sorbet/rbi/gems/ast.rbi +49 -0
- data/sorbet/rbi/gems/ast@2.4.3.rbi +585 -0
- data/sorbet/rbi/gems/benchmark@0.4.0.rbi +618 -0
- data/sorbet/rbi/gems/builder.rbi +58 -0
- data/sorbet/rbi/gems/builder@3.3.0.rbi +502 -0
- data/sorbet/rbi/gems/citrus.rbi +238 -0
- data/sorbet/rbi/gems/citrus@3.0.2.rbi +1234 -0
- data/sorbet/rbi/gems/colorize.rbi +82 -0
- data/sorbet/rbi/gems/colorize@0.8.1.rbi +152 -0
- data/sorbet/rbi/gems/curses.rbi +327 -0
- data/sorbet/rbi/gems/curses@1.4.7.rbi +894 -0
- data/sorbet/rbi/gems/diffy.rbi +56 -0
- data/sorbet/rbi/gems/diffy@3.4.3.rbi +184 -0
- data/sorbet/rbi/gems/docile.rbi +36 -0
- data/sorbet/rbi/gems/docile@1.4.1.rbi +377 -0
- data/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
- data/sorbet/rbi/gems/fiddle.rbi +120 -0
- data/sorbet/rbi/gems/fiddle@1.1.6.rbi +9 -0
- data/sorbet/rbi/gems/hank.rbi +15 -0
- data/sorbet/rbi/gems/json@2.10.2.rbi +2112 -0
- data/sorbet/rbi/gems/language_server-protocol.rbi +2868 -0
- data/sorbet/rbi/gems/language_server-protocol@3.17.0.4.rbi +9 -0
- data/sorbet/rbi/gems/lint_roller.rbi +75 -0
- data/sorbet/rbi/gems/lint_roller@1.1.0.rbi +86 -0
- data/sorbet/rbi/gems/logger.rbi +94 -0
- data/sorbet/rbi/gems/logger@1.7.0.rbi +963 -0
- data/sorbet/rbi/gems/minitest-reporters.rbi +104 -0
- data/sorbet/rbi/gems/minitest-reporters@1.7.1.rbi +1018 -0
- data/sorbet/rbi/gems/minitest.rbi +440 -0
- data/sorbet/rbi/gems/minitest@5.25.5.rbi +1547 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +159 -0
- data/sorbet/rbi/gems/parallel.rbi +88 -0
- data/sorbet/rbi/gems/parallel@1.26.3.rbi +291 -0
- data/sorbet/rbi/gems/parser.rbi +1545 -0
- data/sorbet/rbi/gems/parser@3.3.7.4.rbi +5535 -0
- data/sorbet/rbi/gems/prism.rbi +4090 -0
- data/sorbet/rbi/gems/prism@1.4.0.rbi +41732 -0
- data/sorbet/rbi/gems/racc.rbi +41 -0
- data/sorbet/rbi/gems/racc@1.8.1.rbi +160 -0
- data/sorbet/rbi/gems/rainbow.rbi +122 -0
- data/sorbet/rbi/gems/rainbow@3.1.1.rbi +403 -0
- data/sorbet/rbi/gems/rake.rbi +649 -0
- data/sorbet/rbi/gems/rake@13.2.1.rbi +3033 -0
- data/sorbet/rbi/gems/rbi@0.3.1.rbi +6599 -0
- data/sorbet/rbi/gems/rbs@3.9.2.rbi +6978 -0
- data/sorbet/rbi/gems/regexp_parser.rbi +1039 -0
- data/sorbet/rbi/gems/regexp_parser@2.10.0.rbi +3795 -0
- data/sorbet/rbi/gems/rubocop-ast.rbi +1470 -0
- data/sorbet/rbi/gems/rubocop-ast@1.44.0.rbi +7716 -0
- data/sorbet/rbi/gems/rubocop.rbi +10557 -0
- data/sorbet/rbi/gems/rubocop@1.75.2.rbi +61860 -0
- data/sorbet/rbi/gems/ruby-filemagic.rbi +46 -0
- data/sorbet/rbi/gems/ruby-filemagic@0.7.3.rbi +168 -0
- data/sorbet/rbi/gems/ruby-progressbar.rbi +321 -0
- data/sorbet/rbi/gems/ruby-progressbar@1.13.0.rbi +1318 -0
- data/sorbet/rbi/gems/simplecov-html.rbi +36 -0
- data/sorbet/rbi/gems/simplecov-html@0.13.1.rbi +225 -0
- data/sorbet/rbi/gems/simplecov.rbi +435 -0
- data/sorbet/rbi/gems/simplecov@0.22.0.rbi +2149 -0
- data/sorbet/rbi/gems/simplecov_json_formatter.rbi +47 -0
- data/sorbet/rbi/gems/simplecov_json_formatter@0.1.4.rbi +9 -0
- data/sorbet/rbi/gems/spoom@1.6.1.rbi +7274 -0
- data/sorbet/rbi/gems/tapioca@0.16.11.rbi +3628 -0
- data/sorbet/rbi/gems/thor.rbi +583 -0
- data/sorbet/rbi/gems/thor@1.3.2.rbi +4378 -0
- data/sorbet/rbi/gems/toml-rb.rbi +136 -0
- data/sorbet/rbi/gems/toml-rb@2.2.0.rbi +419 -0
- data/sorbet/rbi/gems/unicode-display_width.rbi +28 -0
- data/sorbet/rbi/gems/unicode-display_width@3.1.4.rbi +132 -0
- data/sorbet/rbi/gems/unicode-emoji.rbi +18 -0
- data/sorbet/rbi/gems/unicode-emoji@4.0.4.rbi +251 -0
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
- data/sorbet/rbi/gems/yard@0.9.37.rbi +18379 -0
- data/sorbet/tapioca/config.yml +13 -0
- data/sorbet/tapioca/require.rb +4 -0
- metadata +399 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4e219d6cebabc8bf82ab4a659651c32e26fec0307d1a99aa5056209607023e10
|
4
|
+
data.tar.gz: 4322b70a37b63927ecc0e614d7559baacd074728801169b3ea17eb366071fc31
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c0f7a2893fa7c325cebc65cd3dca8afc31a9712b2b7ae6e192d9687ec9221936d0f1a02a63539aedb5b92288028f930c1912f5dcbf19c7258043d78e1b2c0d3f
|
7
|
+
data.tar.gz: 798201a727a850d0c57cd7746978c980b5e770c923a61950c6fbe7bf1406c82a43954caaf96e95bf4255c03283b016b38dd88a013a230286f308f7c0ed640ef8
|
data/.clodignore
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Default .clodignore file for Claude uploader
|
2
|
+
# Add patterns to ignore files when uploading to Claude
|
3
|
+
|
4
|
+
*.dll
|
5
|
+
*.dylib
|
6
|
+
*.exe
|
7
|
+
*.gif
|
8
|
+
*.ico
|
9
|
+
*.jar
|
10
|
+
*.jpg
|
11
|
+
*.jpeg
|
12
|
+
*.mp3
|
13
|
+
*.mp4
|
14
|
+
*.png
|
15
|
+
*.so
|
16
|
+
*.svg
|
17
|
+
*.tar.gz
|
18
|
+
*.zip
|
19
|
+
.clod
|
20
|
+
.git
|
21
|
+
build
|
22
|
+
dist
|
23
|
+
node_modules
|
24
|
+
out
|
25
|
+
target
|
26
|
+
*.log
|
27
|
+
Cargo.lock
|
28
|
+
package-lock.json
|
29
|
+
pnpm-lock.yaml
|
30
|
+
yarn.lock
|
31
|
+
.gitignore
|
32
|
+
.clodignore
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
plugins:
|
2
|
+
- rubocop-minitest
|
3
|
+
- rubocop-rake
|
4
|
+
|
5
|
+
AllCops:
|
6
|
+
NewCops: enable
|
7
|
+
|
8
|
+
Metrics/MethodLength:
|
9
|
+
Exclude:
|
10
|
+
- 'test/**/*'
|
11
|
+
- 'lib/hank/cli.rb'
|
12
|
+
- 'lib/hank/file_selector.rb'
|
13
|
+
- 'lib/hank/path_utils.rb'
|
14
|
+
- 'lib/hank/symlink_manager.rb'
|
15
|
+
- 'lib/hank/validator.rb'
|
16
|
+
|
17
|
+
Metrics/AbcSize:
|
18
|
+
Exclude:
|
19
|
+
- 'test/**/*'
|
20
|
+
- 'lib/hank/cli.rb'
|
21
|
+
- 'lib/hank/file_selector.rb'
|
22
|
+
- 'lib/hank/symlink_manager.rb'
|
23
|
+
- 'lib/hank/validator.rb'
|
24
|
+
|
25
|
+
Metrics/CyclomaticComplexity:
|
26
|
+
Exclude:
|
27
|
+
- 'lib/hank/cli.rb'
|
28
|
+
- 'lib/hank/file_selector.rb'
|
29
|
+
- 'lib/hank/symlink_manager.rb'
|
30
|
+
|
31
|
+
Metrics/PerceivedComplexity:
|
32
|
+
Exclude:
|
33
|
+
- 'lib/hank/cli.rb'
|
34
|
+
- 'lib/hank/file_selector.rb'
|
35
|
+
- 'lib/hank/symlink_manager.rb'
|
36
|
+
|
37
|
+
Metrics/ClassLength:
|
38
|
+
Exclude:
|
39
|
+
- 'lib/hank/cli.rb'
|
40
|
+
- 'lib/hank/file_selector.rb'
|
41
|
+
|
42
|
+
Metrics/BlockLength:
|
43
|
+
Exclude:
|
44
|
+
- 'lib/hank/file_selector.rb'
|
45
|
+
- 'lib/hank/validator.rb'
|
46
|
+
- 'hank.gemspec'
|
47
|
+
- 'test/**/*'
|
48
|
+
|
49
|
+
Minitest/MultipleAssertions:
|
50
|
+
Exclude:
|
51
|
+
- 'test/**/*'
|
52
|
+
|
53
|
+
Style/Documentation:
|
54
|
+
Exclude:
|
55
|
+
- 'lib/hank/version.rb'
|
56
|
+
- 'test/**/*'
|
57
|
+
|
58
|
+
Gemspec/DevelopmentDependencies:
|
59
|
+
EnforcedStyle: gemspec
|
data/CLAUDE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Build/Test Commands
|
6
|
+
- Run all tests: `bundle exec rake test`
|
7
|
+
- Run single test: `bundle exec ruby -Itest test/hank/specific_test.rb -n test_method_name`
|
8
|
+
- Run linter: `bundle exec rake rubocop`
|
9
|
+
- Default task (tests + lint): `bundle exec rake`
|
10
|
+
- Build gem: `bundle exec rake build`
|
11
|
+
- Install gem locally: `bundle exec rake install`
|
12
|
+
|
13
|
+
## Code Style Guidelines
|
14
|
+
- **Typing**: Uses Sorbet for static typing with `# typed: strict` at top of files and `extend T::Sig`
|
15
|
+
- **Type Signatures**: Methods defined with `sig { params(...).returns(...) }` blocks
|
16
|
+
- **Imports**: Require statements at top with `# frozen_string_literal: true` comment
|
17
|
+
- **Formatting**: 2-space indentation, RuboCop enforced style
|
18
|
+
- **Naming**: Snake_case for methods/variables, CamelCase for classes/modules
|
19
|
+
- **Namespacing**: All classes under `Hank::` module
|
20
|
+
- **Error Handling**: Custom `Hank::Error` class, specific rescue blocks with colorized warnings
|
21
|
+
- **Testing**: Minitest framework with `_test.rb` suffix in `test/` directory mirroring lib structure
|
22
|
+
- **Dependencies**: Managed in gemspec with versioned requirements
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Fuzz Leonard
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# Hank
|
2
|
+
|
3
|
+
[](https://badge.fury.io/rb/hank)
|
4
|
+
|
5
|
+
Hank manages symlinks between source paths and flattened pathnames within a git repository, making it easier to manage configuration files and collaborate with AI tools.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Install the gem and add to the application's Gemfile by executing:
|
10
|
+
|
11
|
+
```bash
|
12
|
+
$ gem install hank
|
13
|
+
```
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Run the `hank` command in the directory where you want to manage your files:
|
18
|
+
|
19
|
+
```bash
|
20
|
+
$ hank
|
21
|
+
```
|
22
|
+
|
23
|
+
If no Hankfile exists, Hank will offer to create one and guide you through selecting files to manage.
|
24
|
+
|
25
|
+
To edit an existing Hankfile:
|
26
|
+
|
27
|
+
```bash
|
28
|
+
$ hank edit
|
29
|
+
```
|
30
|
+
|
31
|
+
Check the version:
|
32
|
+
|
33
|
+
```bash
|
34
|
+
$ hank --version
|
35
|
+
```
|
36
|
+
|
37
|
+
## How It Works
|
38
|
+
|
39
|
+
Hank maintains a Hankfile (in TOML format) that maps source paths to flattened pathnames within your repository. When you run `hank`, it ensures all source paths are symlinks pointing to their corresponding targets in your repository.
|
40
|
+
|
41
|
+
For example, if you want to manage your `.bashrc` file, Hank will:
|
42
|
+
|
43
|
+
1. Create a file named `dot--bashrc` in your repository
|
44
|
+
2. Create a symlink from `~/.bashrc` to this file
|
45
|
+
|
46
|
+
This allows you to version control your configuration files while keeping them in their original locations.
|
47
|
+
|
48
|
+
## Features
|
49
|
+
|
50
|
+
- Manages symlinks between source paths and repository files
|
51
|
+
- Converts dotfiles to visible files with "dot--" prefix
|
52
|
+
- Flattens pathnames for better organization
|
53
|
+
- Detects text files using libmagic
|
54
|
+
- Provides a simple GUI for file selection
|
55
|
+
- Integrates with git for version control
|
56
|
+
- Includes comprehensive test coverage
|
57
|
+
|
58
|
+
## Development
|
59
|
+
|
60
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
61
|
+
|
62
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
63
|
+
|
64
|
+
## Contributing
|
65
|
+
|
66
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/fuzz/hank.
|
67
|
+
|
68
|
+
## License
|
69
|
+
|
70
|
+
The gem is available as open source under the terms of the MIT License.
|
data/Rakefile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rubocop/rake_task'
|
6
|
+
require 'rubocop-minitest'
|
7
|
+
require 'rubocop-rake'
|
8
|
+
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'test'
|
11
|
+
t.libs << 'lib'
|
12
|
+
t.test_files = FileList['test/**/*_test.rb']
|
13
|
+
end
|
14
|
+
|
15
|
+
RuboCop::RakeTask.new
|
16
|
+
|
17
|
+
task default: %i[test rubocop]
|
data/bin/hank
ADDED
data/hank.gemspec
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'lib/hank/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'hank'
|
7
|
+
spec.version = Hank::VERSION
|
8
|
+
spec.authors = ['Fuzz Leonard']
|
9
|
+
spec.email = ['ink@fuzz.ink']
|
10
|
+
|
11
|
+
spec.summary = 'Symlink manager for easier file management and AI collaboration'
|
12
|
+
spec.description = 'Hank manages symlinks between source paths and flattened pathnames within a git repository, ' \
|
13
|
+
'making it easier to manage configuration files and collaborate with AI tools.'
|
14
|
+
spec.homepage = 'https://github.com/fuzz/hank'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
spec.required_ruby_version = '>= 2.7.0'
|
17
|
+
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
19
|
+
spec.metadata['source_code_uri'] = 'https://github.com/fuzz/hank'
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/fuzz/hank/blob/main/CHANGELOG.md'
|
21
|
+
|
22
|
+
# Specify which files should be added to the gem when it is released.
|
23
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
24
|
+
spec.files = Dir.chdir(__dir__) do
|
25
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
26
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
27
|
+
end
|
28
|
+
end
|
29
|
+
spec.bindir = 'bin'
|
30
|
+
spec.executables = ['hank']
|
31
|
+
spec.require_paths = ['lib']
|
32
|
+
|
33
|
+
# Dependencies
|
34
|
+
spec.add_dependency 'colorize', '~> 0.8'
|
35
|
+
spec.add_dependency 'curses', '~> 1.4'
|
36
|
+
spec.add_dependency 'diffy', '~> 3.4'
|
37
|
+
spec.add_dependency 'fiddle', '~> 1.1'
|
38
|
+
spec.add_dependency 'logger', '~> 1.5'
|
39
|
+
spec.add_dependency 'ruby-filemagic', '~> 0.7'
|
40
|
+
spec.add_dependency 'sorbet-runtime', '~> 0.5'
|
41
|
+
spec.add_dependency 'thor', '~> 1.2'
|
42
|
+
spec.add_dependency 'toml-rb', '~> 2.2'
|
43
|
+
|
44
|
+
# Development dependencies
|
45
|
+
spec.add_development_dependency 'minitest', '~> 5.16'
|
46
|
+
spec.add_development_dependency 'minitest-reporters', '~> 1.5'
|
47
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
48
|
+
spec.add_development_dependency 'rubocop', '~> 1.36'
|
49
|
+
spec.add_development_dependency 'rubocop-minitest', '~> 0.38'
|
50
|
+
spec.add_development_dependency 'rubocop-rake', '~> 0.7'
|
51
|
+
spec.add_development_dependency 'simplecov', '~> 0.21'
|
52
|
+
spec.add_development_dependency 'sorbet', '~> 0.5'
|
53
|
+
spec.add_development_dependency 'tapioca', '~> 0.16'
|
54
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
55
|
+
end
|
data/lib/hank/cli.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
module Hank
|
5
|
+
# Command line interface for Hank
|
6
|
+
class CLI < Thor
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
desc '', 'Verify or create symlinks based on Hankfile'
|
10
|
+
sig { void }
|
11
|
+
def index
|
12
|
+
hankfile_path = Pathname.new(Dir.pwd).join('Hankfile')
|
13
|
+
|
14
|
+
if hankfile_path.exist?
|
15
|
+
process_existing_hankfile(hankfile_path)
|
16
|
+
else
|
17
|
+
create_new_hankfile(hankfile_path)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
default_task :index
|
21
|
+
|
22
|
+
desc 'edit', 'Edit Hankfile using GUI file browser'
|
23
|
+
sig { returns(NilClass) }
|
24
|
+
def edit
|
25
|
+
hankfile_path = Pathname.new(Dir.pwd).join('Hankfile')
|
26
|
+
|
27
|
+
unless hankfile_path.exist?
|
28
|
+
puts "No Hankfile found in current directory. Run 'hank' to create one.".red
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
|
32
|
+
hankfile = Hankfile.from_file(hankfile_path)
|
33
|
+
file_selector = FileSelector.new(hankfile.mappings.keys)
|
34
|
+
selected_files = file_selector.run
|
35
|
+
|
36
|
+
if selected_files
|
37
|
+
hankfile.update_mappings(selected_files)
|
38
|
+
puts 'Hankfile updated with selected files.'
|
39
|
+
else
|
40
|
+
puts 'Edit cancelled.'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
map '--version' => :version
|
45
|
+
desc '--version', 'Display version information'
|
46
|
+
sig { returns(NilClass) }
|
47
|
+
def version
|
48
|
+
puts "Hank version #{Hank::VERSION}"
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
sig { params(hankfile_path: Pathname).void }
|
54
|
+
def process_existing_hankfile(hankfile_path)
|
55
|
+
hankfile = Hankfile.from_file(hankfile_path)
|
56
|
+
symlink_manager = SymlinkManager.new(hankfile)
|
57
|
+
validator = Validator.new(hankfile)
|
58
|
+
|
59
|
+
issues = validator.validate
|
60
|
+
|
61
|
+
if issues.empty?
|
62
|
+
puts 'All symlinks are correctly configured.'.green
|
63
|
+
else
|
64
|
+
puts "Found #{issues.size} issues:".yellow
|
65
|
+
|
66
|
+
issues.each do |issue|
|
67
|
+
handle_issue(issue, symlink_manager, hankfile_path.to_s)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
sig { params(issue: Validator::Issue, symlink_manager: SymlinkManager, _hankfile_path: T.nilable(String)).void }
|
73
|
+
def handle_issue(issue, symlink_manager, _hankfile_path = nil)
|
74
|
+
source_path = issue.source_path
|
75
|
+
target_path = issue.target_path
|
76
|
+
|
77
|
+
case issue.type
|
78
|
+
when :missing
|
79
|
+
puts "Creating symlink for #{source_path} -> #{target_path}".yellow
|
80
|
+
symlink_manager.create_symlink(source_path, target_path)
|
81
|
+
when :wrong_target
|
82
|
+
puts "Symlink #{source_path} points to wrong target:".red
|
83
|
+
puts " Current: #{issue.current_target}"
|
84
|
+
puts " Expected: #{target_path}"
|
85
|
+
puts 'Options:'
|
86
|
+
puts ' 1. Update symlink to point to correct target'
|
87
|
+
puts ' 2. Remove from Hankfile'
|
88
|
+
print 'Choose option (1/2): '
|
89
|
+
choice = gets.chomp
|
90
|
+
|
91
|
+
if choice == '1'
|
92
|
+
symlink_manager.create_symlink(source_path, target_path, force: true)
|
93
|
+
elsif choice == '2'
|
94
|
+
symlink_manager.remove_mapping(source_path)
|
95
|
+
else
|
96
|
+
puts 'Invalid choice. Skipping.'.red
|
97
|
+
end
|
98
|
+
when :not_symlink
|
99
|
+
if File.directory?(source_path)
|
100
|
+
puts "#{source_path} is a directory, skipping.".yellow
|
101
|
+
else
|
102
|
+
puts "#{source_path} is a regular file, not a symlink:".red
|
103
|
+
target_path_full = symlink_manager.base_dir.join(target_path).to_s
|
104
|
+
if File.file?(source_path) && File.file?(target_path_full)
|
105
|
+
diff = Diffy::Diff.new(source_path, target_path_full,
|
106
|
+
source: 'files').to_s(:color)
|
107
|
+
puts diff
|
108
|
+
end
|
109
|
+
puts 'Options:'
|
110
|
+
puts ' 1. Replace with symlink (backup existing file)'
|
111
|
+
puts ' 2. Remove from Hankfile'
|
112
|
+
print 'Choose option (1/2): '
|
113
|
+
choice = gets.chomp
|
114
|
+
|
115
|
+
if choice == '1'
|
116
|
+
symlink_manager.create_symlink(source_path, target_path, force: true)
|
117
|
+
elsif choice == '2'
|
118
|
+
symlink_manager.remove_mapping(source_path)
|
119
|
+
else
|
120
|
+
puts 'Invalid choice. Skipping.'.red
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
sig { params(_hankfile_path: Pathname).void }
|
127
|
+
def create_new_hankfile(_hankfile_path)
|
128
|
+
puts 'No Hankfile found in current directory.'
|
129
|
+
print 'Would you like to create one? (y/n): '
|
130
|
+
|
131
|
+
answer = gets.chomp.downcase
|
132
|
+
return unless answer == 'y'
|
133
|
+
|
134
|
+
file_selector = FileSelector.new
|
135
|
+
selected_files = file_selector.run
|
136
|
+
|
137
|
+
return puts 'No files selected. Exiting.'.yellow if selected_files.nil? || selected_files.empty?
|
138
|
+
|
139
|
+
hankfile = Hankfile.new
|
140
|
+
Pathname.new(Dir.pwd)
|
141
|
+
|
142
|
+
selected_files.each do |source_path|
|
143
|
+
target_path = PathUtils.flatten_path(source_path)
|
144
|
+
hankfile.add_mapping(source_path, target_path)
|
145
|
+
end
|
146
|
+
|
147
|
+
puts "\nProposed Hankfile contents:"
|
148
|
+
puts hankfile
|
149
|
+
|
150
|
+
print "\nWould you like to proceed to copy source files and create symlinks? (y/n): "
|
151
|
+
answer = gets.chomp.downcase
|
152
|
+
|
153
|
+
if answer == 'y'
|
154
|
+
symlink_manager = SymlinkManager.new(hankfile)
|
155
|
+
symlink_manager.install_all
|
156
|
+
puts 'Hankfile created and symlinks installed.'.green
|
157
|
+
else
|
158
|
+
puts 'Operation cancelled.'.yellow
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
require 'curses'
|
5
|
+
|
6
|
+
module Hank
|
7
|
+
# Interactive curses-based file selection interface
|
8
|
+
class FileSelector
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { params(initial_files: T.nilable(T::Array[String])).void }
|
12
|
+
def initialize(initial_files = nil)
|
13
|
+
@initial_files = T.let(initial_files || [], T::Array[String])
|
14
|
+
@selected_files = T.let(@initial_files.dup, T::Array[String])
|
15
|
+
@current_path = T.let(Pathname.new('/'), Pathname)
|
16
|
+
@file_list = T.let([], T::Array[String])
|
17
|
+
@current_index = T.let(0, Integer)
|
18
|
+
@offset = T.let(0, Integer)
|
19
|
+
end
|
20
|
+
|
21
|
+
sig { returns(T.nilable(T::Array[String])) }
|
22
|
+
def run
|
23
|
+
init_curses
|
24
|
+
begin
|
25
|
+
load_files
|
26
|
+
draw_screen
|
27
|
+
handle_input
|
28
|
+
ensure
|
29
|
+
close_curses
|
30
|
+
end
|
31
|
+
|
32
|
+
@selected_files.empty? ? nil : @selected_files
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
sig { void }
|
38
|
+
def init_curses
|
39
|
+
Curses.init_screen
|
40
|
+
Curses.start_color
|
41
|
+
Curses.curs_set(0) # Hide cursor
|
42
|
+
Curses.noecho # Don't echo input
|
43
|
+
Curses.cbreak # Immediate key input
|
44
|
+
Curses.stdscr.keypad(true) # Enable arrow keys
|
45
|
+
|
46
|
+
# Define color pairs
|
47
|
+
Curses.init_pair(1, Curses::COLOR_WHITE, Curses::COLOR_BLUE) # Header/footer
|
48
|
+
Curses.init_pair(2, Curses::COLOR_BLACK, Curses::COLOR_WHITE) # Selected item
|
49
|
+
Curses.init_pair(3, Curses::COLOR_GREEN, Curses::COLOR_BLACK) # Directory
|
50
|
+
Curses.init_pair(4, Curses::COLOR_WHITE, Curses::COLOR_BLACK) # File
|
51
|
+
Curses.init_pair(5, Curses::COLOR_YELLOW, Curses::COLOR_BLACK) # Selected file
|
52
|
+
end
|
53
|
+
|
54
|
+
sig { void }
|
55
|
+
def close_curses
|
56
|
+
Curses.close_screen
|
57
|
+
end
|
58
|
+
|
59
|
+
sig { void }
|
60
|
+
def load_files
|
61
|
+
@file_list = []
|
62
|
+
|
63
|
+
# Add parent directory option
|
64
|
+
@file_list << '..'
|
65
|
+
|
66
|
+
# Add directories
|
67
|
+
dirs = Dir.entries(@current_path.to_s)
|
68
|
+
.select { |entry| File.directory?(File.join(@current_path, entry)) && entry != '.' && entry != '..' }
|
69
|
+
.sort
|
70
|
+
.map { |entry| "#{entry}/" }
|
71
|
+
|
72
|
+
@file_list.concat(dirs)
|
73
|
+
|
74
|
+
# Add text files
|
75
|
+
files = Dir.entries(@current_path.to_s)
|
76
|
+
.select do |entry|
|
77
|
+
path = File.join(@current_path, entry)
|
78
|
+
File.file?(path) && !File.symlink?(path) && PathUtils.text_file?(path)
|
79
|
+
end
|
80
|
+
.sort
|
81
|
+
|
82
|
+
@file_list.concat(files)
|
83
|
+
|
84
|
+
# Reset current index if needed
|
85
|
+
@current_index = 0 if @current_index >= @file_list.length
|
86
|
+
@offset = 0 if @offset >= @file_list.length
|
87
|
+
end
|
88
|
+
|
89
|
+
sig { void }
|
90
|
+
def draw_screen
|
91
|
+
Curses.clear
|
92
|
+
height = Curses.lines
|
93
|
+
width = Curses.cols
|
94
|
+
|
95
|
+
# Draw header
|
96
|
+
Curses.attron(Curses.color_pair(1))
|
97
|
+
Curses.setpos(0, 0)
|
98
|
+
Curses.addstr(' ' * width)
|
99
|
+
Curses.setpos(0, 0)
|
100
|
+
Curses.addstr(" Hank File Selector - #{@current_path}")
|
101
|
+
Curses.attroff(Curses.color_pair(1))
|
102
|
+
|
103
|
+
# Draw file list
|
104
|
+
visible_items = height - 4 # Header, footer, and help line
|
105
|
+
@offset = @current_index - visible_items + 1 if @current_index >= @offset + visible_items
|
106
|
+
@offset = @current_index if @current_index < @offset
|
107
|
+
|
108
|
+
visible_files = @file_list[@offset, visible_items]
|
109
|
+
visible_files&.each_with_index do |file, idx|
|
110
|
+
row = idx + 1
|
111
|
+
real_idx = idx + @offset
|
112
|
+
is_selected = @current_index == real_idx
|
113
|
+
is_dir = file.end_with?('/')
|
114
|
+
is_chosen = @selected_files.include?(File.join(@current_path, file.chomp('/')).to_s)
|
115
|
+
|
116
|
+
# Select color based on file type and selection status
|
117
|
+
if is_selected
|
118
|
+
Curses.attron(Curses.color_pair(2))
|
119
|
+
elsif is_dir
|
120
|
+
Curses.attron(Curses.color_pair(3))
|
121
|
+
elsif is_chosen
|
122
|
+
Curses.attron(Curses.color_pair(5))
|
123
|
+
else
|
124
|
+
Curses.attron(Curses.color_pair(4))
|
125
|
+
end
|
126
|
+
|
127
|
+
Curses.setpos(row, 0)
|
128
|
+
Curses.addstr(' ' * width)
|
129
|
+
Curses.setpos(row, 0)
|
130
|
+
prefix = is_chosen ? '[*] ' : '[ ] '
|
131
|
+
Curses.addstr("#{prefix}#{file}")
|
132
|
+
|
133
|
+
# Reset attributes
|
134
|
+
if is_selected
|
135
|
+
Curses.attroff(Curses.color_pair(2))
|
136
|
+
elsif is_dir
|
137
|
+
Curses.attroff(Curses.color_pair(3))
|
138
|
+
elsif is_chosen
|
139
|
+
Curses.attroff(Curses.color_pair(5))
|
140
|
+
else
|
141
|
+
Curses.attroff(Curses.color_pair(4))
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Draw help line
|
146
|
+
Curses.setpos(height - 2, 0)
|
147
|
+
Curses.addstr(' ' * width)
|
148
|
+
Curses.setpos(height - 2, 0)
|
149
|
+
Curses.addstr(' ↑/↓: Navigate | Space: Select | Enter: Open | q: Quit | d: Done')
|
150
|
+
|
151
|
+
# Draw footer with stats
|
152
|
+
Curses.attron(Curses.color_pair(1))
|
153
|
+
Curses.setpos(height - 1, 0)
|
154
|
+
Curses.addstr(' ' * width)
|
155
|
+
Curses.setpos(height - 1, 0)
|
156
|
+
Curses.addstr(" Selected: #{@selected_files.size} files")
|
157
|
+
Curses.attroff(Curses.color_pair(1))
|
158
|
+
|
159
|
+
Curses.refresh
|
160
|
+
end
|
161
|
+
|
162
|
+
sig { returns(T.nilable(T::Array[String])) }
|
163
|
+
def handle_input
|
164
|
+
loop do
|
165
|
+
draw_screen
|
166
|
+
key = Curses.getch
|
167
|
+
|
168
|
+
case key
|
169
|
+
when Curses::KEY_UP
|
170
|
+
@current_index -= 1 if @current_index.positive?
|
171
|
+
when Curses::KEY_DOWN
|
172
|
+
@current_index += 1 if @current_index < @file_list.length - 1
|
173
|
+
when Curses::KEY_ENTER, 10, 13 # Enter key
|
174
|
+
file = @file_list[@current_index]
|
175
|
+
if file == '..'
|
176
|
+
@current_path = @current_path.parent
|
177
|
+
load_files
|
178
|
+
elsif T.must(file).end_with?('/')
|
179
|
+
@current_path = @current_path.join(T.must(file).chomp('/'))
|
180
|
+
load_files
|
181
|
+
end
|
182
|
+
when ' ' # Space key for selection
|
183
|
+
file = @file_list[@current_index]
|
184
|
+
next if file == '..' # Skip parent directory
|
185
|
+
|
186
|
+
path = if T.must(file).end_with?('/') # Directory
|
187
|
+
File.join(@current_path, T.must(file).chomp('/')).to_s
|
188
|
+
else # File
|
189
|
+
File.join(@current_path, file).to_s
|
190
|
+
end
|
191
|
+
if @selected_files.include?(path)
|
192
|
+
@selected_files.delete(path)
|
193
|
+
else
|
194
|
+
@selected_files << path
|
195
|
+
end
|
196
|
+
when 'q', 'Q'
|
197
|
+
return nil # Cancel
|
198
|
+
when 'd', 'D'
|
199
|
+
return @selected_files
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|