bundler-skills 0.3.0 → 0.4.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/CHANGELOG.md +25 -0
- data/PROPOSAL.md +16 -5
- data/README.md +50 -51
- data/exe/skills +7 -0
- data/lib/bundler_skills/cli.rb +151 -0
- data/lib/bundler_skills/config.rb +0 -9
- data/lib/bundler_skills/disabling.rb +9 -37
- data/lib/bundler_skills/linker.rb +18 -3
- data/lib/bundler_skills/rubygems_hook.rb +51 -0
- data/lib/bundler_skills/synchronizer.rb +31 -1
- data/lib/bundler_skills/version.rb +1 -1
- data/lib/bundler_skills.rb +0 -1
- data/lib/rubygems_plugin.rb +13 -0
- metadata +12 -9
- data/lib/bundler_skills/command.rb +0 -126
- data/lib/bundler_skills/hook.rb +0 -31
- data/plugins.rb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 962c265c0293c266b9fcca439681d9850cdac4332348dd2d1ffe11a4e5580111
|
|
4
|
+
data.tar.gz: 495a9a9f1f2a1c0da2cb02aa14b73c1902b0584e64647066a9da967615b0c4af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4cad5c43044a98626b9fab90cb799c167e012f56d4c53c3839153648e3daef3f6c9cd10f1fa77771a2a0af8bedccbee77204054bcde6e48b2682fe9a36ca0c4c
|
|
7
|
+
data.tar.gz: 6ec0164595551b1710069fe015151462cee20b87f614ed7b8c5d87d21053e7374332fde34e670196b8b0e0a7e2d1a6491f56b8be22ba09bd2047a7341957ce33
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-06-27
|
|
6
|
+
|
|
7
|
+
### Changed (breaking)
|
|
8
|
+
|
|
9
|
+
- **bundler-skills is now a regular gem, not a Bundler plugin.** Install it as
|
|
10
|
+
`gem "bundler-skills"` (recommended: in the `development` group) instead of
|
|
11
|
+
`plugin "bundler-skills"`. This removes the `Bundler::Plugin::Index::CommandConflict`
|
|
12
|
+
that occurred (and recurred on every `bundle` run until `bundler plugin
|
|
13
|
+
uninstall`) whenever `bundle update` bumped bundler-skills itself.
|
|
14
|
+
- Syncing now runs from a RubyGems `Gem.post_install` hook
|
|
15
|
+
(`lib/rubygems_plugin.rb`): when a gem is actually installed during
|
|
16
|
+
`bundle install` / `bundle update`, only **that gem's** skills are linked, and
|
|
17
|
+
only that gem's own stale links are pruned (other gems are untouched). A
|
|
18
|
+
cached `bundle install` that installs nothing does no work.
|
|
19
|
+
- The manual command moved from the Bundler subcommand `bundle skills` to the
|
|
20
|
+
plain executable **`bundle exec skills`** (`sync` / `list` / `clean` / `init`,
|
|
21
|
+
with `--dry-run`). `skills sync` scans all gems and prunes all stale links.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
|
|
25
|
+
- Production / CI auto-detection (`RAILS_ENV`/`RACK_ENV=production`, `CI`) and the
|
|
26
|
+
`enabled:` config key / `BUNDLER_SKILLS_ENABLED` override. With a
|
|
27
|
+
development-group install the gem isn't present in production/CI anyway. The
|
|
28
|
+
only switch left is `BUNDLER_SKILLS_DISABLED`.
|
|
29
|
+
|
|
5
30
|
## [0.3.0] - 2026-06-21
|
|
6
31
|
|
|
7
32
|
### Changed
|
data/PROPOSAL.md
CHANGED
|
@@ -76,8 +76,19 @@ even for gems like `rails-html-sanitizer`.
|
|
|
76
76
|
linked too (the directory is symlinked as a whole), so reference relative
|
|
77
77
|
files normally.
|
|
78
78
|
|
|
79
|
-
## Why a
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
## Why a RubyGems `post_install` hook (not a Bundler plugin)
|
|
80
|
+
|
|
81
|
+
bundler-skills ships as a regular gem with a `lib/rubygems_plugin.rb` that
|
|
82
|
+
registers a `Gem.post_install` hook. The hook **does** fire during
|
|
83
|
+
`bundle install` for each gem that is actually installed (only `Gem.done_installing`
|
|
84
|
+
is Bundler-skipped), so it is a reliable place to sync that gem's skills.
|
|
85
|
+
|
|
86
|
+
An earlier version was a Bundler plugin that registered a `bundle skills`
|
|
87
|
+
command via `Bundler::Plugin::API.command`. That had a fatal flaw: when
|
|
88
|
+
`bundle update` bumped bundler-skills itself, Bundler re-registered the `skills`
|
|
89
|
+
command while the previous registration was still in its plugin index, raising
|
|
90
|
+
`Bundler::Plugin::Index::CommandConflict` — and it recurred on every subsequent
|
|
91
|
+
`bundle` run until `bundler plugin uninstall`. This is unavoidable for any plugin
|
|
92
|
+
that registers a command. Becoming a regular gem removes the command registration
|
|
93
|
+
entirely (the manual command is now the plain executable `bundle exec skills`),
|
|
94
|
+
so the conflict cannot happen. See [README.md](README.md) for consumer details.
|
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# bundler-skills
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
|
|
3
|
+
A gem that auto-symlinks **AI agent skills bundled in your gems** into your
|
|
4
|
+
project on `bundle install`. The Ruby/Bundler counterpart of
|
|
5
5
|
[antfu/skills-npm](https://github.com/antfu/skills-npm).
|
|
6
6
|
|
|
7
7
|
Gems ship `skills/<name>/SKILL.md`; your project links them into the right agent
|
|
@@ -10,15 +10,20 @@ team gets them just by running `bundle install`.
|
|
|
10
10
|
|
|
11
11
|
## How it works
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
bundler-skills ships a RubyGems `post_install` hook (via
|
|
14
|
+
`lib/rubygems_plugin.rb`). Whenever a gem is **actually installed** during
|
|
15
|
+
`bundle install` / `bundle update`, the hook runs for that gem and:
|
|
14
16
|
|
|
15
|
-
1. Scans
|
|
17
|
+
1. Scans **that gem** for `skills/*/SKILL.md`.
|
|
16
18
|
2. Detects which agents you use (by marker directories) and symlinks each skill
|
|
17
19
|
into the right place, named `gem-<gem>--<skill>`.
|
|
18
|
-
3.
|
|
20
|
+
3. Prunes that gem's own stale links (so a version that renames or drops a skill
|
|
21
|
+
updates correctly), leaving every other gem's links untouched.
|
|
22
|
+
4. Adds the generated symlink patterns to `.gitignore` (they are machine-local).
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
A `bundle install` that installs nothing (everything already cached) does no
|
|
25
|
+
work — only freshly installed gems are processed. To re-sync everything at once
|
|
26
|
+
(e.g. after removing a gem), run `bundle exec skills` (see below).
|
|
22
27
|
|
|
23
28
|
### Supported agents
|
|
24
29
|
|
|
@@ -36,13 +41,16 @@ marker exists, so nothing is created in projects that don't use these tools.
|
|
|
36
41
|
|
|
37
42
|
## Installation
|
|
38
43
|
|
|
39
|
-
Add the
|
|
44
|
+
Add the gem to your `Gemfile`, in the `development` group (skills are a
|
|
45
|
+
development-time concern; this is the recommended, team-wide way):
|
|
40
46
|
|
|
41
47
|
```ruby
|
|
42
48
|
# Gemfile
|
|
43
49
|
source "https://rubygems.org"
|
|
44
50
|
|
|
45
|
-
|
|
51
|
+
group :development do
|
|
52
|
+
gem "bundler-skills"
|
|
53
|
+
end
|
|
46
54
|
|
|
47
55
|
gem "some-gem-that-ships-skills"
|
|
48
56
|
```
|
|
@@ -53,58 +61,53 @@ Then:
|
|
|
53
61
|
bundle install
|
|
54
62
|
```
|
|
55
63
|
|
|
56
|
-
That's it.
|
|
64
|
+
That's it. When a skill-bearing gem is installed you'll see something like:
|
|
57
65
|
|
|
58
66
|
```
|
|
59
|
-
[bundler-skills]
|
|
67
|
+
[bundler-skills] 1 skill(s) discovered, 1 linked, 0 relinked, 0 pruned across 1 dir(s) (agents: claude)
|
|
68
|
+
created:
|
|
69
|
+
.claude/skills/gem-rubocop--style -> /path/to/gems/rubocop/skills/style
|
|
60
70
|
```
|
|
61
71
|
|
|
72
|
+
> In production / CI you typically run `bundle install` with the `development`
|
|
73
|
+
> group excluded (`bundle config set --local without development`), so the gem
|
|
74
|
+
> isn't even present and nothing runs. You can also force it off anywhere with
|
|
75
|
+
> `BUNDLER_SKILLS_DISABLED=1`.
|
|
76
|
+
|
|
62
77
|
When a run actually changes something (links created, relinked after a gem
|
|
63
78
|
update, or stale links pruned) the summary is printed in green so it stands out;
|
|
64
79
|
a run with nothing to do prints the same line in plain text. A changed run also
|
|
65
80
|
lists each affected skill, grouped by kind, with the path to its `SKILL.md` so
|
|
66
|
-
you can review the (third-party) skill contents now linked into your project
|
|
81
|
+
you can review the (third-party) skill contents now linked into your project.
|
|
67
82
|
|
|
68
|
-
|
|
69
|
-
[bundler-skills] 3 skill(s) discovered, 2 linked, 1 relinked, 0 pruned across 1 dir(s) (agents: claude)
|
|
70
|
-
created:
|
|
71
|
-
.claude/skills/gem-rubocop--style -> /path/to/gems/rubocop/skills/style
|
|
72
|
-
.claude/skills/gem-rubocop--lint -> /path/to/gems/rubocop/skills/lint
|
|
73
|
-
relinked:
|
|
74
|
-
.claude/skills/gem-rspec--matchers -> /path/to/gems/rspec/skills/matchers
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
> Alternatively, install it globally with `bundle plugin install bundler-skills`.
|
|
78
|
-
> The `Gemfile` approach is preferred because it propagates to the whole team.
|
|
79
|
-
|
|
80
|
-
## The `bundle skills` command
|
|
83
|
+
## The `bundle exec skills` command
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
The `post_install` hook only fires for gems that are freshly installed. To
|
|
86
|
+
re-sync everything at once — after removing a gem, after `bundle lock` (which
|
|
87
|
+
installs nothing), or just to inspect/clean — run the command:
|
|
85
88
|
|
|
86
89
|
```sh
|
|
87
|
-
bundle skills # (or:
|
|
88
|
-
bundle skills list # show discovered skills and target agents (no changes)
|
|
89
|
-
bundle skills clean # remove all gem-*--* symlinks this
|
|
90
|
-
bundle skills init # create a bundler-skills.yml config file with defaults
|
|
91
|
-
bundle skills <cmd> --dry-run # show what would change without writing
|
|
90
|
+
bundle exec skills # (or: skills sync) re-scan ALL gems and (re)create symlinks
|
|
91
|
+
bundle exec skills list # show discovered skills and target agents (no changes)
|
|
92
|
+
bundle exec skills clean # remove all gem-*--* symlinks this gem created
|
|
93
|
+
bundle exec skills init # create a bundler-skills.yml config file with defaults
|
|
94
|
+
bundle exec skills <cmd> --dry-run # show what would change without writing
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
Unlike the automatic hook,
|
|
95
|
-
|
|
97
|
+
Unlike the automatic hook, `skills sync` scans every resolved gem and prunes any
|
|
98
|
+
stale `gem-*--*` link (so it also cleans up after a removed gem). It always runs
|
|
99
|
+
and ignores `BUNDLER_SKILLS_DISABLED`, since invoking it is an explicit action.
|
|
96
100
|
|
|
97
101
|
## Configuration
|
|
98
102
|
|
|
99
103
|
All optional. Create `bundler-skills.yml` in your project root:
|
|
100
104
|
|
|
101
105
|
```yaml
|
|
102
|
-
enabled: # nil (auto) | false (off) | [development] (env list)
|
|
103
106
|
agents: # omit = auto-detect; or list: [claude, cursor]; or "*"
|
|
104
107
|
- claude
|
|
105
108
|
- cursor
|
|
106
109
|
gitignore: true # manage .gitignore (default true)
|
|
107
|
-
cleanup: true # prune stale gem-*--* links
|
|
110
|
+
cleanup: true # prune stale gem-*--* links (default true)
|
|
108
111
|
recursive: false # also scan skills/**/SKILL.md (default false)
|
|
109
112
|
include: # only these gems (empty = all). fnmatch on "gem" or "gem/skill"
|
|
110
113
|
- rubocop
|
|
@@ -115,24 +118,15 @@ exclude: # exclude these (wins over include)
|
|
|
115
118
|
|
|
116
119
|
Notes:
|
|
117
120
|
|
|
118
|
-
- Skills from `development`/`test` group gems are included by default — those are
|
|
119
|
-
exactly the gems (linters, test helpers) that ship skills. They are only
|
|
120
|
-
excluded when your environment sets `bundle config set without development`
|
|
121
|
-
(e.g. production), in which case skills aren't wanted anyway.
|
|
122
121
|
- The link target is the gem's path on your machine; that's why the symlinks are
|
|
123
122
|
gitignored and re-created on each machine's `bundle install`.
|
|
124
123
|
|
|
125
124
|
### Disabling
|
|
126
125
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
- `CI` is truthy
|
|
132
|
-
- `bundler-skills.yml` has `enabled: false`, or `enabled: [..]` not listing the
|
|
133
|
-
current env
|
|
134
|
-
|
|
135
|
-
Force it on with `BUNDLER_SKILLS_ENABLED=1` or `enabled: true`.
|
|
126
|
+
Set `BUNDLER_SKILLS_DISABLED` to a truthy value (`1`/`true`/`yes`/`on`) to turn
|
|
127
|
+
the `post_install` hook off. There is no production/CI auto-detection: with the
|
|
128
|
+
recommended `development`-group install the gem isn't present in production/CI in
|
|
129
|
+
the first place. The manual `bundle exec skills` command ignores this switch.
|
|
136
130
|
|
|
137
131
|
## Naming: `gem-<gem>--<skill>`
|
|
138
132
|
|
|
@@ -155,7 +149,7 @@ See [PROPOSAL.md](PROPOSAL.md) for the distribution convention. In short: put
|
|
|
155
149
|
|
|
156
150
|
## Trust boundary
|
|
157
151
|
|
|
158
|
-
Skills bundled in third-party gems are third-party content. This
|
|
152
|
+
Skills bundled in third-party gems are third-party content. This gem only
|
|
159
153
|
**creates symlinks** to them — it never executes their contents. Reviewing what a
|
|
160
154
|
skill instructs your agent to do is the user's responsibility, the same as
|
|
161
155
|
reviewing any dependency.
|
|
@@ -165,6 +159,11 @@ reviewing any dependency.
|
|
|
165
159
|
- POSIX symlinks are assumed; Windows is not supported yet.
|
|
166
160
|
- Whether Cursor / Codex / Copilot follow symlinked `SKILL.md` during their own
|
|
167
161
|
directory scans is not formally documented; verified working with Claude Code.
|
|
162
|
+
- The `post_install` hook only fires for gems that are **actually installed**. A
|
|
163
|
+
cached `bundle install` (nothing to install) and `bundle lock` do no syncing —
|
|
164
|
+
run `bundle exec skills` to re-sync on demand.
|
|
165
|
+
- When a gem is **removed**, no hook fires for it, so its `gem-*--*` links linger
|
|
166
|
+
until the next `bundle exec skills` (full sync prunes them).
|
|
168
167
|
|
|
169
168
|
## Development
|
|
170
169
|
|
data/exe/skills
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module BundlerSkills
|
|
6
|
+
# `bundle exec skills [sync|list|clean|init] [--dry-run]`
|
|
7
|
+
#
|
|
8
|
+
# The manual entry point (a plain executable, not a Bundler plugin command).
|
|
9
|
+
# Unlike the post_install hook it ignores the disable switch — running it is an
|
|
10
|
+
# explicit user action — and it reuses the same Synchronizer logic.
|
|
11
|
+
class CLI
|
|
12
|
+
INIT_TEMPLATE = <<~YAML
|
|
13
|
+
# bundler-skills.yml — all keys are optional
|
|
14
|
+
#
|
|
15
|
+
# agents: # omit = auto-detect; or list: [claude, cursor]; or "*"
|
|
16
|
+
# - claude
|
|
17
|
+
# - cursor
|
|
18
|
+
# gitignore: true # manage .gitignore (default true)
|
|
19
|
+
# cleanup: true # prune stale gem-*--* links when a gem is removed (default true)
|
|
20
|
+
# recursive: false # also scan skills/**/SKILL.md (default false)
|
|
21
|
+
# include: # only these gems (empty = all). fnmatch on "gem" or "gem/skill"
|
|
22
|
+
# - rubocop
|
|
23
|
+
# - "rails-*"
|
|
24
|
+
# exclude: # exclude these (wins over include)
|
|
25
|
+
# - some-noisy-gem
|
|
26
|
+
YAML
|
|
27
|
+
|
|
28
|
+
USAGE = <<~HELP
|
|
29
|
+
Usage: bundle exec skills [SUBCOMMAND] [--dry-run]
|
|
30
|
+
|
|
31
|
+
sync (default) discover skills and (re)create symlinks
|
|
32
|
+
list show discovered skills and target agents (no changes)
|
|
33
|
+
clean remove all gem-*--* symlinks this gem created
|
|
34
|
+
init create a bundler-skills.yml config file with defaults
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
--dry-run show what would change without writing
|
|
38
|
+
-h, --help show this help
|
|
39
|
+
HELP
|
|
40
|
+
|
|
41
|
+
def initialize(logger: StdoutLogger.new)
|
|
42
|
+
@logger = logger
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param argv [Array<String>]
|
|
46
|
+
# @return [Integer] process exit status
|
|
47
|
+
def run(argv)
|
|
48
|
+
args = argv.dup
|
|
49
|
+
opts = { dry_run: false }
|
|
50
|
+
parser = OptionParser.new do |o|
|
|
51
|
+
o.on("--dry-run") { opts[:dry_run] = true }
|
|
52
|
+
o.on("-h", "--help") { @logger.info(USAGE); return 0 }
|
|
53
|
+
end
|
|
54
|
+
parser.order!(args)
|
|
55
|
+
|
|
56
|
+
subcommand = args.shift || "sync"
|
|
57
|
+
case subcommand
|
|
58
|
+
when "sync" then run_sync(opts[:dry_run])
|
|
59
|
+
when "list" then run_list
|
|
60
|
+
when "clean" then run_clean(opts[:dry_run])
|
|
61
|
+
when "init" then run_init
|
|
62
|
+
when "help" then @logger.info(USAGE)
|
|
63
|
+
else
|
|
64
|
+
@logger.error("[bundler-skills] unknown subcommand: #{subcommand}")
|
|
65
|
+
@logger.info(USAGE)
|
|
66
|
+
return 1
|
|
67
|
+
end
|
|
68
|
+
0
|
|
69
|
+
rescue OptionParser::ParseError => e
|
|
70
|
+
@logger.error("[bundler-skills] #{e.message}")
|
|
71
|
+
@logger.info(USAGE)
|
|
72
|
+
1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def synchronizer(dry_run: false)
|
|
78
|
+
config = Config.load
|
|
79
|
+
config = OverrideDryRun.new(config) if dry_run
|
|
80
|
+
Synchronizer.new(config: config, logger: @logger)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def run_sync(dry_run)
|
|
84
|
+
synchronizer(dry_run: dry_run).sync
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def run_list
|
|
88
|
+
result = synchronizer.plan
|
|
89
|
+
if result.discovered.empty?
|
|
90
|
+
@logger.info("[bundler-skills] no skills found in dependency gems")
|
|
91
|
+
return
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
agents = result.agents.map(&:key)
|
|
95
|
+
@logger.info("[bundler-skills] #{result.discovered.size} skill(s) " \
|
|
96
|
+
"-> agents: #{agents.empty? ? '(none detected)' : agents.join(', ')}")
|
|
97
|
+
result.discovered.sort_by(&:link_name).each do |skill|
|
|
98
|
+
@logger.info(" #{skill.link_name} -> #{skill.source_path}")
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def run_init
|
|
103
|
+
path = File.join(Bundler.root.to_s, Config::CONFIG_FILENAME)
|
|
104
|
+
if File.exist?(path)
|
|
105
|
+
@logger.warn("[bundler-skills] #{Config::CONFIG_FILENAME} already exists")
|
|
106
|
+
return
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
File.write(path, INIT_TEMPLATE)
|
|
110
|
+
@logger.info("[bundler-skills] created #{Config::CONFIG_FILENAME}")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def run_clean(dry_run)
|
|
114
|
+
removed = synchronizer(dry_run: dry_run).clean
|
|
115
|
+
total = removed.values.sum(&:size)
|
|
116
|
+
verb = dry_run ? "would remove" : "removed"
|
|
117
|
+
@logger.info("[bundler-skills] #{verb} #{total} link(s)")
|
|
118
|
+
removed.each do |subdir, names|
|
|
119
|
+
names.each { |n| @logger.info(" #{subdir}/#{n}") }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Minimal logger matching the subset of Bundler.ui that Synchronizer/Linker
|
|
124
|
+
# use (info/warn/error/confirm), writing to stdout/stderr.
|
|
125
|
+
class StdoutLogger
|
|
126
|
+
def info(message) = $stdout.puts(message)
|
|
127
|
+
def confirm(message) = $stdout.puts(message)
|
|
128
|
+
def warn(message) = $stderr.puts(message)
|
|
129
|
+
def error(message) = $stderr.puts(message)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Wraps a Config to force dry_run? true without mutating the original.
|
|
133
|
+
class OverrideDryRun
|
|
134
|
+
def initialize(config)
|
|
135
|
+
@config = config
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def dry_run?
|
|
139
|
+
true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def respond_to_missing?(name, include_private = false)
|
|
143
|
+
@config.respond_to?(name, include_private) || super
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def method_missing(name, *args, &block)
|
|
147
|
+
@config.send(name, *args, &block)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
@@ -5,7 +5,6 @@ module BundlerSkills
|
|
|
5
5
|
#
|
|
6
6
|
# The file is optional: a missing file yields an all-defaults Config so the
|
|
7
7
|
# hook works out of the box. Supported keys:
|
|
8
|
-
# enabled nil(auto) | true | false | [env names]
|
|
9
8
|
# agents nil(auto-detect) | "*" | [keys] | "key"
|
|
10
9
|
# gitignore bool (default true)
|
|
11
10
|
# cleanup bool (default true) — prune stale gem-*--* links
|
|
@@ -17,7 +16,6 @@ module BundlerSkills
|
|
|
17
16
|
CONFIG_FILENAME = "bundler-skills.yml"
|
|
18
17
|
|
|
19
18
|
DEFAULTS = {
|
|
20
|
-
"enabled" => nil,
|
|
21
19
|
"agents" => nil,
|
|
22
20
|
"gitignore" => true,
|
|
23
21
|
"cleanup" => true,
|
|
@@ -49,13 +47,6 @@ module BundlerSkills
|
|
|
49
47
|
@data = data
|
|
50
48
|
end
|
|
51
49
|
|
|
52
|
-
# nil | true | false | Array<String>. A bare string env name is normalized
|
|
53
|
-
# to a one-element array so `enabled: development` behaves like a list.
|
|
54
|
-
def enabled
|
|
55
|
-
value = @data["enabled"]
|
|
56
|
-
value.is_a?(String) ? [value] : value
|
|
57
|
-
end
|
|
58
|
-
|
|
59
50
|
# nil (auto-detect) | Array<String>. A bare string key is wrapped so
|
|
60
51
|
# `agents: claude` works as well as a YAML list.
|
|
61
52
|
def agents
|
|
@@ -1,36 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module BundlerSkills
|
|
4
|
-
# Decides whether the
|
|
4
|
+
# Decides whether the post_install hook should run.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# The hook only fires when a gem is actually installed, and in the
|
|
7
|
+
# recommended development-group setup the gem isn't even present in
|
|
8
|
+
# production / CI — so there is no environment auto-detection here. The single
|
|
9
|
+
# escape hatch is the BUNDLER_SKILLS_DISABLED env var. The manual CLI ignores
|
|
10
|
+
# this entirely (running it is an explicit user action).
|
|
10
11
|
module Disabling
|
|
11
12
|
TRUTHY = %w[1 true yes on].freeze
|
|
12
13
|
|
|
13
14
|
module_function
|
|
14
15
|
|
|
15
16
|
# @param env [Hash] environment variables (defaults to ENV)
|
|
16
|
-
# @
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
# Explicit overrides win over everything else.
|
|
20
|
-
return true if truthy?(env["BUNDLER_SKILLS_DISABLED"])
|
|
21
|
-
return false if truthy?(env["BUNDLER_SKILLS_ENABLED"])
|
|
22
|
-
|
|
23
|
-
enabled = config&.enabled
|
|
24
|
-
case enabled
|
|
25
|
-
when false
|
|
26
|
-
return true
|
|
27
|
-
when Array
|
|
28
|
-
return true unless enabled.map(&:to_s).include?(current_environment(env))
|
|
29
|
-
when true
|
|
30
|
-
return false
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
production?(env) || ci?(env)
|
|
17
|
+
# @return [Boolean] true when the hook must not run
|
|
18
|
+
def disabled?(env: ENV)
|
|
19
|
+
truthy?(env["BUNDLER_SKILLS_DISABLED"])
|
|
34
20
|
end
|
|
35
21
|
|
|
36
22
|
def truthy?(value)
|
|
@@ -38,19 +24,5 @@ module BundlerSkills
|
|
|
38
24
|
|
|
39
25
|
TRUTHY.include?(value.to_s.strip.downcase)
|
|
40
26
|
end
|
|
41
|
-
|
|
42
|
-
def production?(env)
|
|
43
|
-
%w[RAILS_ENV RACK_ENV].any? { |key| env[key].to_s.strip.downcase == "production" }
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def ci?(env)
|
|
47
|
-
truthy?(env["CI"])
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Best-effort current environment name for `enabled:` list matching.
|
|
51
|
-
def current_environment(env)
|
|
52
|
-
value = env["RAILS_ENV"] || env["RACK_ENV"]
|
|
53
|
-
value.to_s.strip.empty? ? "development" : value.strip.downcase
|
|
54
|
-
end
|
|
55
27
|
end
|
|
56
28
|
end
|
|
@@ -36,14 +36,20 @@ module BundlerSkills
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# @param skills [Array<DiscoveredSkill>]
|
|
39
|
+
# @param prune_scope [Symbol, Array<String>, nil] which stale links to prune:
|
|
40
|
+
# :all (default) -> every gem-*--* link we own (full sync)
|
|
41
|
+
# [prefix, ...] -> only links whose basename starts with one of the
|
|
42
|
+
# given prefixes, e.g. "gem-<name>--" (single-gem sync)
|
|
43
|
+
# nil -> prune nothing
|
|
44
|
+
# Pruning still honors @config.cleanup? — when cleanup is off nothing is pruned.
|
|
39
45
|
# @return [Result]
|
|
40
|
-
def link(skills)
|
|
46
|
+
def link(skills, prune_scope: :all)
|
|
41
47
|
result = Result.new
|
|
42
48
|
link_names = skills.map(&:link_name)
|
|
43
49
|
|
|
44
50
|
ensure_dir
|
|
45
51
|
skills.each { |skill| link_one(skill, result) }
|
|
46
|
-
prune_stale(link_names, result) if @config.cleanup?
|
|
52
|
+
prune_stale(link_names, result, prune_scope) if @config.cleanup? && prune_scope
|
|
47
53
|
result
|
|
48
54
|
end
|
|
49
55
|
|
|
@@ -97,10 +103,13 @@ module BundlerSkills
|
|
|
97
103
|
|
|
98
104
|
# Remove gem-*--* symlinks we own that are no longer in the discovery set.
|
|
99
105
|
# Real directories, other prefixes, and unmanaged symlinks are left alone.
|
|
100
|
-
|
|
106
|
+
# When +scope+ is an array of prefixes, only links whose basename starts with
|
|
107
|
+
# one of them are considered (so a single-gem sync never touches other gems).
|
|
108
|
+
def prune_stale(valid_link_names, result, scope = :all)
|
|
101
109
|
Dir.glob(File.join(@skills_dir, STALE_GLOB)).each do |path|
|
|
102
110
|
name = File.basename(path)
|
|
103
111
|
next if valid_link_names.include?(name)
|
|
112
|
+
next unless in_scope?(name, scope)
|
|
104
113
|
next unless File.symlink?(path) # only prune our own symlinks
|
|
105
114
|
|
|
106
115
|
remove(path)
|
|
@@ -108,6 +117,12 @@ module BundlerSkills
|
|
|
108
117
|
end
|
|
109
118
|
end
|
|
110
119
|
|
|
120
|
+
def in_scope?(name, scope)
|
|
121
|
+
return true if scope == :all
|
|
122
|
+
|
|
123
|
+
Array(scope).any? { |prefix| name.start_with?(prefix) }
|
|
124
|
+
end
|
|
125
|
+
|
|
111
126
|
def replace_symlink(link_path, target)
|
|
112
127
|
remove(link_path)
|
|
113
128
|
create_symlink(link_path, target)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BundlerSkills
|
|
4
|
+
# Entry point for the RubyGems `Gem.post_install` hook (registered in
|
|
5
|
+
# lib/rubygems_plugin.rb). Fires once per gem actually installed during a
|
|
6
|
+
# `bundle install` / `bundle update`, and syncs THAT gem's skills only.
|
|
7
|
+
#
|
|
8
|
+
# Like the old Bundler hook, it holds no real logic: it guards on context and
|
|
9
|
+
# the disable switch, then delegates to Synchronizer#sync_gem. Any error is
|
|
10
|
+
# swallowed as a warning so it never aborts the user's install.
|
|
11
|
+
module RubygemsHook
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @param installer [#spec] a Gem::Installer (or anything exposing #spec)
|
|
15
|
+
def install(installer)
|
|
16
|
+
spec = installer.spec
|
|
17
|
+
return unless bundle_context?
|
|
18
|
+
return if Disabling.disabled?
|
|
19
|
+
|
|
20
|
+
Synchronizer.new(config: Config.load).sync_gem(spec)
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
warn("[bundler-skills] skipped #{safe_name(installer)}: #{e.class}: #{e.message}")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Only act inside a `bundle install` for a project with a Gemfile. This
|
|
26
|
+
# keeps a plain `gem install foo` (no project context) from creating
|
|
27
|
+
# symlinks in whatever directory the user happens to be in.
|
|
28
|
+
def bundle_context?
|
|
29
|
+
return false unless defined?(Bundler)
|
|
30
|
+
|
|
31
|
+
root = Bundler.root
|
|
32
|
+
root.join("Gemfile").file? || root.join("gems.rb").file?
|
|
33
|
+
rescue StandardError
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def safe_name(installer)
|
|
38
|
+
installer.spec.name
|
|
39
|
+
rescue StandardError
|
|
40
|
+
"(unknown gem)"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def warn(message)
|
|
44
|
+
if defined?(Bundler)
|
|
45
|
+
Bundler.ui.warn(message)
|
|
46
|
+
else
|
|
47
|
+
Kernel.warn(message)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -25,7 +25,37 @@ module BundlerSkills
|
|
|
25
25
|
|
|
26
26
|
links_by_dir = subdirs.to_h do |subdir|
|
|
27
27
|
skills_dir = File.join(@root.to_s, subdir)
|
|
28
|
-
|
|
28
|
+
linker = Linker.new(skills_dir: skills_dir, config: @config, logger: @logger)
|
|
29
|
+
[subdir, linker.link(skills, prune_scope: :all)]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
gitignore_changed = update_gitignore(subdirs)
|
|
33
|
+
|
|
34
|
+
log_summary(skills, agents, links_by_dir)
|
|
35
|
+
Result.new(
|
|
36
|
+
discovered: skills, agents: agents,
|
|
37
|
+
links_by_dir: links_by_dir, gitignore_changed: gitignore_changed
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Sync the skills of a SINGLE gem (used by the RubyGems post_install hook).
|
|
42
|
+
#
|
|
43
|
+
# Only links belonging to this gem (gem-<name>--*) are added/updated, and
|
|
44
|
+
# only this gem's stale links are pruned — every other gem's links are left
|
|
45
|
+
# untouched. So when a gem's new version drops or renames a skill, its old
|
|
46
|
+
# link is removed, but unrelated gems are never disturbed.
|
|
47
|
+
#
|
|
48
|
+
# @param spec [#name, #full_gem_path] a Gem::Specification (or compatible)
|
|
49
|
+
def sync_gem(spec)
|
|
50
|
+
skills = Discoverer.new(specs: [spec], config: @config, logger: @logger).discover
|
|
51
|
+
agents = AgentRegistry.resolve(@root, @config)
|
|
52
|
+
subdirs = AgentRegistry.output_subdirs(agents)
|
|
53
|
+
scope = ["#{DiscoveredSkill::LINK_PREFIX}#{spec.name}#{DiscoveredSkill::BOUNDARY}"]
|
|
54
|
+
|
|
55
|
+
links_by_dir = subdirs.to_h do |subdir|
|
|
56
|
+
skills_dir = File.join(@root.to_s, subdir)
|
|
57
|
+
linker = Linker.new(skills_dir: skills_dir, config: @config, logger: @logger)
|
|
58
|
+
[subdir, linker.link(skills, prune_scope: scope)]
|
|
29
59
|
end
|
|
30
60
|
|
|
31
61
|
gitignore_changed = update_gitignore(subdirs)
|
data/lib/bundler_skills.rb
CHANGED
|
@@ -9,7 +9,6 @@ require_relative "bundler_skills/agent_registry"
|
|
|
9
9
|
require_relative "bundler_skills/linker"
|
|
10
10
|
require_relative "bundler_skills/gitignore_updater"
|
|
11
11
|
require_relative "bundler_skills/synchronizer"
|
|
12
|
-
require_relative "bundler_skills/hook"
|
|
13
12
|
|
|
14
13
|
module BundlerSkills
|
|
15
14
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# RubyGems loads this file automatically (it is on the require path and named
|
|
4
|
+
# by the rubygems_plugin convention) during `bundle install` / `gem install`.
|
|
5
|
+
# We register a post_install hook that syncs each freshly installed gem's
|
|
6
|
+
# skills. This replaces the old Bundler plugin (`command "skills"` caused a
|
|
7
|
+
# CommandConflict whenever the plugin updated itself).
|
|
8
|
+
require_relative "bundler_skills"
|
|
9
|
+
require_relative "bundler_skills/rubygems_hook"
|
|
10
|
+
|
|
11
|
+
Gem.post_install do |installer|
|
|
12
|
+
BundlerSkills::RubygemsHook.install(installer)
|
|
13
|
+
end
|
metadata
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bundler-skills
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- aki77
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description: A
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
description: A gem that discovers skills/ directories bundled in your dependency gems
|
|
13
|
+
and symlinks them into your project's agent skill directories (.claude/skills, .agents/skills)
|
|
14
|
+
on bundle install, via a RubyGems post_install hook. The Ruby/Bundler counterpart
|
|
15
|
+
of antfu/skills-npm.
|
|
15
16
|
email:
|
|
16
17
|
- aki77@users.noreply.github.com
|
|
17
|
-
executables:
|
|
18
|
+
executables:
|
|
19
|
+
- skills
|
|
18
20
|
extensions: []
|
|
19
21
|
extra_rdoc_files: []
|
|
20
22
|
files:
|
|
@@ -22,19 +24,20 @@ files:
|
|
|
22
24
|
- LICENSE.txt
|
|
23
25
|
- PROPOSAL.md
|
|
24
26
|
- README.md
|
|
27
|
+
- exe/skills
|
|
25
28
|
- lib/bundler_skills.rb
|
|
26
29
|
- lib/bundler_skills/agent_registry.rb
|
|
27
|
-
- lib/bundler_skills/
|
|
30
|
+
- lib/bundler_skills/cli.rb
|
|
28
31
|
- lib/bundler_skills/config.rb
|
|
29
32
|
- lib/bundler_skills/disabling.rb
|
|
30
33
|
- lib/bundler_skills/discovered_skill.rb
|
|
31
34
|
- lib/bundler_skills/discoverer.rb
|
|
32
35
|
- lib/bundler_skills/gitignore_updater.rb
|
|
33
|
-
- lib/bundler_skills/hook.rb
|
|
34
36
|
- lib/bundler_skills/linker.rb
|
|
37
|
+
- lib/bundler_skills/rubygems_hook.rb
|
|
35
38
|
- lib/bundler_skills/synchronizer.rb
|
|
36
39
|
- lib/bundler_skills/version.rb
|
|
37
|
-
-
|
|
40
|
+
- lib/rubygems_plugin.rb
|
|
38
41
|
homepage: https://github.com/aki77/bundler-skills
|
|
39
42
|
licenses:
|
|
40
43
|
- MIT
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module BundlerSkills
|
|
4
|
-
# `bundle skills [sync|list|clean] [--dry-run]`
|
|
5
|
-
#
|
|
6
|
-
# The manual entry point. Unlike the hook, it ignores the production/CI
|
|
7
|
-
# disabling guard — running it is an explicit user action. It reuses the
|
|
8
|
-
# same Synchronizer logic the hook uses.
|
|
9
|
-
class Command < Bundler::Plugin::API
|
|
10
|
-
command "skills"
|
|
11
|
-
|
|
12
|
-
INIT_TEMPLATE = <<~YAML
|
|
13
|
-
# bundler-skills.yml — all keys are optional
|
|
14
|
-
#
|
|
15
|
-
# enabled: # nil (auto) | false (off) | [development] (env list)
|
|
16
|
-
# agents: # omit = auto-detect; or list: [claude, cursor]; or "*"
|
|
17
|
-
# - claude
|
|
18
|
-
# - cursor
|
|
19
|
-
# gitignore: true # manage .gitignore (default true)
|
|
20
|
-
# cleanup: true # prune stale gem-*--* links when a gem is removed (default true)
|
|
21
|
-
# recursive: false # also scan skills/**/SKILL.md (default false)
|
|
22
|
-
# include: # only these gems (empty = all). fnmatch on "gem" or "gem/skill"
|
|
23
|
-
# - rubocop
|
|
24
|
-
# - "rails-*"
|
|
25
|
-
# exclude: # exclude these (wins over include)
|
|
26
|
-
# - some-noisy-gem
|
|
27
|
-
YAML
|
|
28
|
-
|
|
29
|
-
def exec(_command_name, args)
|
|
30
|
-
dry_run = !!args.delete("--dry-run")
|
|
31
|
-
subcommand = args.shift || "sync"
|
|
32
|
-
|
|
33
|
-
case subcommand
|
|
34
|
-
when "sync" then run_sync(dry_run)
|
|
35
|
-
when "list" then run_list
|
|
36
|
-
when "clean" then run_clean(dry_run)
|
|
37
|
-
when "init" then run_init
|
|
38
|
-
when "help", "-h", "--help" then print_help
|
|
39
|
-
else
|
|
40
|
-
Bundler.ui.error("[bundler-skills] unknown subcommand: #{subcommand}")
|
|
41
|
-
print_help
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
def synchronizer(dry_run: false)
|
|
48
|
-
config = Config.load
|
|
49
|
-
config = OverrideDryRun.new(config) if dry_run
|
|
50
|
-
Synchronizer.new(config: config)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def run_sync(dry_run)
|
|
54
|
-
synchronizer(dry_run: dry_run).sync
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def run_list
|
|
58
|
-
result = synchronizer.plan
|
|
59
|
-
if result.discovered.empty?
|
|
60
|
-
Bundler.ui.info("[bundler-skills] no skills found in dependency gems")
|
|
61
|
-
return
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
agents = result.agents.map(&:key)
|
|
65
|
-
Bundler.ui.info("[bundler-skills] #{result.discovered.size} skill(s) " \
|
|
66
|
-
"-> agents: #{agents.empty? ? '(none detected)' : agents.join(', ')}")
|
|
67
|
-
result.discovered.sort_by(&:link_name).each do |skill|
|
|
68
|
-
Bundler.ui.info(" #{skill.link_name} -> #{skill.source_path}")
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def run_init
|
|
73
|
-
path = Bundler.root / Config::CONFIG_FILENAME
|
|
74
|
-
if File.exist?(path)
|
|
75
|
-
Bundler.ui.warn("[bundler-skills] #{Config::CONFIG_FILENAME} already exists")
|
|
76
|
-
return
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
File.write(path, INIT_TEMPLATE)
|
|
80
|
-
Bundler.ui.info("[bundler-skills] created #{Config::CONFIG_FILENAME}")
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def run_clean(dry_run)
|
|
84
|
-
removed = synchronizer(dry_run: dry_run).clean
|
|
85
|
-
total = removed.values.sum(&:size)
|
|
86
|
-
verb = dry_run ? "would remove" : "removed"
|
|
87
|
-
Bundler.ui.info("[bundler-skills] #{verb} #{total} link(s)")
|
|
88
|
-
removed.each do |subdir, names|
|
|
89
|
-
names.each { |n| Bundler.ui.info(" #{subdir}/#{n}") }
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def print_help
|
|
94
|
-
Bundler.ui.info(<<~HELP)
|
|
95
|
-
Usage: bundle skills [SUBCOMMAND] [--dry-run]
|
|
96
|
-
|
|
97
|
-
sync (default) discover skills and (re)create symlinks
|
|
98
|
-
list show discovered skills and target agents (no changes)
|
|
99
|
-
clean remove all gem-*--* symlinks this plugin created
|
|
100
|
-
init create a bundler-skills.yml config file with defaults
|
|
101
|
-
|
|
102
|
-
Options:
|
|
103
|
-
--dry-run show what would change without writing
|
|
104
|
-
HELP
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Wraps a Config to force dry_run? true without mutating the original.
|
|
108
|
-
class OverrideDryRun
|
|
109
|
-
def initialize(config)
|
|
110
|
-
@config = config
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def dry_run?
|
|
114
|
-
true
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def respond_to_missing?(name, include_private = false)
|
|
118
|
-
@config.respond_to?(name, include_private) || super
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def method_missing(name, *args, &block)
|
|
122
|
-
@config.send(name, *args, &block)
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
end
|
data/lib/bundler_skills/hook.rb
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module BundlerSkills
|
|
4
|
-
# Registers the after-install-all hook.
|
|
5
|
-
#
|
|
6
|
-
# The hook itself holds no logic: it guards on Disabling (skip in
|
|
7
|
-
# production/CI), then delegates to Synchronizer. Any error is logged as a
|
|
8
|
-
# warning so it never aborts the user's `bundle install`.
|
|
9
|
-
#
|
|
10
|
-
# NOTE: the block argument of after-install-all is an Array<Bundler::Dependency>,
|
|
11
|
-
# NOT specs. We intentionally ignore it and read Bundler.load.specs inside
|
|
12
|
-
# the Synchronizer instead.
|
|
13
|
-
module Hook
|
|
14
|
-
module_function
|
|
15
|
-
|
|
16
|
-
def register
|
|
17
|
-
Bundler::Plugin.add_hook("after-install-all") do |_dependencies|
|
|
18
|
-
call
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def call
|
|
23
|
-
config = Config.load
|
|
24
|
-
return if Disabling.disabled?(config: config)
|
|
25
|
-
|
|
26
|
-
Synchronizer.new(config: config).sync
|
|
27
|
-
rescue StandardError => e
|
|
28
|
-
Bundler.ui.warn("[bundler-skills] skipped: #{e.class}: #{e.message}")
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|