bundler-skills 0.2.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 +37 -0
- data/PROPOSAL.md +16 -5
- data/README.md +54 -40
- 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 +23 -3
- data/lib/bundler_skills/rubygems_hook.rb +51 -0
- data/lib/bundler_skills/synchronizer.rb +65 -5
- 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,43 @@
|
|
|
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
|
+
|
|
30
|
+
## [0.3.0] - 2026-06-21
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Post-install summary now highlights runs that actually changed something:
|
|
35
|
+
when skills are created, relinked (after a gem update), or pruned, the summary
|
|
36
|
+
line is printed in green (`confirm`) instead of plain text, and the count of
|
|
37
|
+
relinked skills is now shown alongside linked/pruned.
|
|
38
|
+
- A changed run additionally lists each affected skill grouped by kind
|
|
39
|
+
(created / relinked / pruned), showing the path to its `SKILL.md`, so the
|
|
40
|
+
third-party skill contents linked into your project are easy to review.
|
|
41
|
+
|
|
5
42
|
## [0.2.0] - 2026-06-18
|
|
6
43
|
|
|
7
44
|
### Added
|
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,43 +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
|
|
|
62
|
-
>
|
|
63
|
-
>
|
|
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`.
|
|
64
76
|
|
|
65
|
-
|
|
77
|
+
When a run actually changes something (links created, relinked after a gem
|
|
78
|
+
update, or stale links pruned) the summary is printed in green so it stands out;
|
|
79
|
+
a run with nothing to do prints the same line in plain text. A changed run also
|
|
80
|
+
lists each affected skill, grouped by kind, with the path to its `SKILL.md` so
|
|
81
|
+
you can review the (third-party) skill contents now linked into your project.
|
|
66
82
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
83
|
+
## The `bundle exec skills` command
|
|
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:
|
|
70
88
|
|
|
71
89
|
```sh
|
|
72
|
-
bundle skills # (or:
|
|
73
|
-
bundle skills list # show discovered skills and target agents (no changes)
|
|
74
|
-
bundle skills clean # remove all gem-*--* symlinks this
|
|
75
|
-
bundle skills init # create a bundler-skills.yml config file with defaults
|
|
76
|
-
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
|
|
77
95
|
```
|
|
78
96
|
|
|
79
|
-
Unlike the automatic hook,
|
|
80
|
-
|
|
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.
|
|
81
100
|
|
|
82
101
|
## Configuration
|
|
83
102
|
|
|
84
103
|
All optional. Create `bundler-skills.yml` in your project root:
|
|
85
104
|
|
|
86
105
|
```yaml
|
|
87
|
-
enabled: # nil (auto) | false (off) | [development] (env list)
|
|
88
106
|
agents: # omit = auto-detect; or list: [claude, cursor]; or "*"
|
|
89
107
|
- claude
|
|
90
108
|
- cursor
|
|
91
109
|
gitignore: true # manage .gitignore (default true)
|
|
92
|
-
cleanup: true # prune stale gem-*--* links
|
|
110
|
+
cleanup: true # prune stale gem-*--* links (default true)
|
|
93
111
|
recursive: false # also scan skills/**/SKILL.md (default false)
|
|
94
112
|
include: # only these gems (empty = all). fnmatch on "gem" or "gem/skill"
|
|
95
113
|
- rubocop
|
|
@@ -100,24 +118,15 @@ exclude: # exclude these (wins over include)
|
|
|
100
118
|
|
|
101
119
|
Notes:
|
|
102
120
|
|
|
103
|
-
- Skills from `development`/`test` group gems are included by default — those are
|
|
104
|
-
exactly the gems (linters, test helpers) that ship skills. They are only
|
|
105
|
-
excluded when your environment sets `bundle config set without development`
|
|
106
|
-
(e.g. production), in which case skills aren't wanted anyway.
|
|
107
121
|
- The link target is the gem's path on your machine; that's why the symlinks are
|
|
108
122
|
gitignored and re-created on each machine's `bundle install`.
|
|
109
123
|
|
|
110
124
|
### Disabling
|
|
111
125
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
- `CI` is truthy
|
|
117
|
-
- `bundler-skills.yml` has `enabled: false`, or `enabled: [..]` not listing the
|
|
118
|
-
current env
|
|
119
|
-
|
|
120
|
-
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.
|
|
121
130
|
|
|
122
131
|
## Naming: `gem-<gem>--<skill>`
|
|
123
132
|
|
|
@@ -140,7 +149,7 @@ See [PROPOSAL.md](PROPOSAL.md) for the distribution convention. In short: put
|
|
|
140
149
|
|
|
141
150
|
## Trust boundary
|
|
142
151
|
|
|
143
|
-
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
|
|
144
153
|
**creates symlinks** to them — it never executes their contents. Reviewing what a
|
|
145
154
|
skill instructs your agent to do is the user's responsibility, the same as
|
|
146
155
|
reviewing any dependency.
|
|
@@ -150,6 +159,11 @@ reviewing any dependency.
|
|
|
150
159
|
- POSIX symlinks are assumed; Windows is not supported yet.
|
|
151
160
|
- Whether Cursor / Codex / Copilot follow symlinked `SKILL.md` during their own
|
|
152
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).
|
|
153
167
|
|
|
154
168
|
## Development
|
|
155
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
|
|
@@ -22,6 +22,11 @@ module BundlerSkills
|
|
|
22
22
|
@skipped = []
|
|
23
23
|
@pruned = []
|
|
24
24
|
end
|
|
25
|
+
|
|
26
|
+
# Did this run actually mutate the filesystem? kept/skipped are no-ops.
|
|
27
|
+
def changed?
|
|
28
|
+
created.any? || relinked.any? || pruned.any?
|
|
29
|
+
end
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
def initialize(skills_dir:, config: Config.new(Config::DEFAULTS), logger: nil)
|
|
@@ -31,14 +36,20 @@ module BundlerSkills
|
|
|
31
36
|
end
|
|
32
37
|
|
|
33
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.
|
|
34
45
|
# @return [Result]
|
|
35
|
-
def link(skills)
|
|
46
|
+
def link(skills, prune_scope: :all)
|
|
36
47
|
result = Result.new
|
|
37
48
|
link_names = skills.map(&:link_name)
|
|
38
49
|
|
|
39
50
|
ensure_dir
|
|
40
51
|
skills.each { |skill| link_one(skill, result) }
|
|
41
|
-
prune_stale(link_names, result) if @config.cleanup?
|
|
52
|
+
prune_stale(link_names, result, prune_scope) if @config.cleanup? && prune_scope
|
|
42
53
|
result
|
|
43
54
|
end
|
|
44
55
|
|
|
@@ -92,10 +103,13 @@ module BundlerSkills
|
|
|
92
103
|
|
|
93
104
|
# Remove gem-*--* symlinks we own that are no longer in the discovery set.
|
|
94
105
|
# Real directories, other prefixes, and unmanaged symlinks are left alone.
|
|
95
|
-
|
|
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)
|
|
96
109
|
Dir.glob(File.join(@skills_dir, STALE_GLOB)).each do |path|
|
|
97
110
|
name = File.basename(path)
|
|
98
111
|
next if valid_link_names.include?(name)
|
|
112
|
+
next unless in_scope?(name, scope)
|
|
99
113
|
next unless File.symlink?(path) # only prune our own symlinks
|
|
100
114
|
|
|
101
115
|
remove(path)
|
|
@@ -103,6 +117,12 @@ module BundlerSkills
|
|
|
103
117
|
end
|
|
104
118
|
end
|
|
105
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
|
+
|
|
106
126
|
def replace_symlink(link_path, target)
|
|
107
127
|
remove(link_path)
|
|
108
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)
|
|
@@ -83,12 +113,42 @@ module BundlerSkills
|
|
|
83
113
|
end
|
|
84
114
|
|
|
85
115
|
created = links_by_dir.values.sum { |r| r.created.size }
|
|
116
|
+
relinked = links_by_dir.values.sum { |r| r.relinked.size }
|
|
86
117
|
pruned = links_by_dir.values.sum { |r| r.pruned.size }
|
|
87
|
-
|
|
118
|
+
|
|
119
|
+
message =
|
|
88
120
|
"[bundler-skills] #{skills.size} skill(s) discovered, " \
|
|
89
|
-
"#{created} linked, #{
|
|
90
|
-
"(agents: #{agents.map(&:key).join(', ')})"
|
|
91
|
-
|
|
121
|
+
"#{created} linked, #{relinked} relinked, #{pruned} pruned " \
|
|
122
|
+
"across #{links_by_dir.size} dir(s) (agents: #{agents.map(&:key).join(', ')})"
|
|
123
|
+
|
|
124
|
+
# Make a run that actually changed something stand out (green) so it is
|
|
125
|
+
# noticed amid bundle's output; an unchanged run stays plain.
|
|
126
|
+
if links_by_dir.values.any?(&:changed?)
|
|
127
|
+
@logger.confirm(message)
|
|
128
|
+
# List the skills that changed so the user can review the (third-party)
|
|
129
|
+
# SKILL.md contents now linked into their project. created/relinked are
|
|
130
|
+
# in the discovery set so we can show their source path; pruned skills
|
|
131
|
+
# are already gone, so we show the name only.
|
|
132
|
+
log_changed_skills(skills, links_by_dir)
|
|
133
|
+
else
|
|
134
|
+
@logger.info(message)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def log_changed_skills(skills, links_by_dir)
|
|
139
|
+
source_by_link = skills.to_h { |s| [s.link_name, s.source_path] }
|
|
140
|
+
|
|
141
|
+
%i[created relinked pruned].each do |kind|
|
|
142
|
+
entries = links_by_dir.flat_map do |subdir, result|
|
|
143
|
+
result.public_send(kind).map { |name| [File.join(subdir, name), source_by_link[name]] }
|
|
144
|
+
end
|
|
145
|
+
next if entries.empty?
|
|
146
|
+
|
|
147
|
+
@logger.info(" #{kind}:")
|
|
148
|
+
entries.each do |path, source|
|
|
149
|
+
@logger.info(source ? " #{path} -> #{source}" : " #{path}")
|
|
150
|
+
end
|
|
151
|
+
end
|
|
92
152
|
end
|
|
93
153
|
end
|
|
94
154
|
end
|
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
|