skill 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d71981592f86798fc080114f847e07e7889bbe32b3cd7d6e41f1830e9c9be3f7
4
- data.tar.gz: e7aa70524f1d82775670aa06c9bddb7073e337f5be30fa27b8f039f8b9b02dc1
3
+ metadata.gz: ee5cb9655ef0c0e762233b03de1732a2d623c3d2c73c3ed194ef4642abbb56da
4
+ data.tar.gz: e868591ccda4402de0f8ee1fd5d6dd69cffa35b46a6d37c3155e4efa4638987a
5
5
  SHA512:
6
- metadata.gz: 8d880355668cece71955dc47f5cce34ac20dfa4155b8fdc66239ba7e5b4de3dc50c5b46ca05c9ceb608e5f14341c58484f7618485891ba4333229d1e6021d508
7
- data.tar.gz: 0a9e6294de982fc8c93464b436b24d31e217f6d9fc3d85d74c89fa2edeab98088470819fc22c41ab1260de1edb4555b103def81ffdf2c2b695a6bd0c3a70ac53
6
+ metadata.gz: c201ca2ec016eadcfd7c169fef7697f08c2f23acec0af9c1c19e35c123864389c6b1068054e0404672684b689e361d86caf01e43d95fb52e1f686ce73c4e82bb
7
+ data.tar.gz: 7c8d2af1802808d86e6a81e30d137d715c1a4386d9d8ca5c606ce5ac1debb91fb815ed998d1bdf9e8ef89dd8f630e1941bfe7384e94c7a81d8d4de16d27ae93d
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  Each release's notes are in [`packaging/release-notes/`](packaging/release-notes) and on [GitHub Releases](https://github.com/crmne/skill/releases).
4
4
 
5
+ ## 0.2.0 (2026-10-17)
6
+
7
+ `--agents` with the `all` and `coding` groups, `skill install` to restore a lockfile, and the global lockfile in `~/.config/skill`. See [the notes](packaging/release-notes/v0.2.0.md).
8
+
5
9
  ## 0.1.0 (2026-10-17)
6
10
 
7
11
  The first release: `add`, `list`, `update`, `remove`, `find`, and `agents`. See [the notes](packaging/release-notes/v0.1.0.md).
data/README.md CHANGED
@@ -91,7 +91,7 @@ Private repositories work with a token in `GITHUB_TOKEN` or `GH_TOKEN`.
91
91
 
92
92
  `skill` looks at your project and installs into every folder it shows signs of using:
93
93
 
94
- | Target | `--agent` | Project folder | Global folder | Detected by |
94
+ | Target | `--agents` | Project folder | Global folder | Detected by |
95
95
  | --- | --- | --- | --- | --- |
96
96
  | RubyLLM | `rubyllm` | `app/skills` | none | `ruby_llm` in the Gemfile or lockfile, or an `app/skills` folder |
97
97
  | Claude Code | `claude-code` | `.claude/skills` | `~/.claude/skills` | `.claude/` or `CLAUDE.md` |
@@ -103,14 +103,26 @@ Private repositories work with a token in `GITHUB_TOKEN` or `GH_TOKEN`.
103
103
 
104
104
  When it finds none, it uses `.agents/skills`, the folder many agents share. `skill agents` lists all 80 targets. Folders several agents share get one copy.
105
105
 
106
- Choose instead of detecting:
106
+ Choose instead of detecting with `--agents` (`-a`), a comma-separated list of groups and agent names:
107
+
108
+ | `--agents` | Installs into |
109
+ | --- | --- |
110
+ | `all` | Every folder detected, RubyLLM's `app/skills` included in a RubyLLM project. The default |
111
+ | `coding` | Every coding agent's folder detected, never RubyLLM's `app/skills` |
112
+ | `ruby_llm` | RubyLLM's `app/skills` only |
113
+ | `claude-code,cursor` | These agents' folders. `skill agents` lists the names |
114
+ | `*` | Every agent `skill agents` lists |
107
115
 
108
116
  ```sh
109
- skill add anthropics/skills@pdf --agent rubyllm
110
- skill add anthropics/skills@pdf --agent claude-code,cursor
117
+ skill add anthropics/skills@pdf --agents coding # your coding agents, not your app
118
+ skill add anthropics/skills@pdf --agents ruby_llm # your app, not your coding agents
119
+ skill add anthropics/skills@pdf --agents claude-code,cursor
120
+ skill add anthropics/skills@pdf --agents coding,ruby_llm
111
121
  skill add anthropics/skills@pdf --dir vendor/skills
112
122
  ```
113
123
 
124
+ `coding` and `all` fall back to `.agents/skills` when they detect nothing. `--agent` still works, as another name for `--agents`.
125
+
114
126
  ### Project or global
115
127
 
116
128
  Inside a project, `skill` installs into the project. Outside one, it installs globally, into your home folder, for the agents you have installed:
@@ -122,7 +134,7 @@ skill add anthropics/skills@pdf
122
134
 
123
135
  ```
124
136
  ✓ Installed pdf → ~/.claude/skills/pdf (global)
125
- Recorded in ~/.local/state/skill/skills.lock
137
+ Recorded in ~/.config/skill/skills.lock
126
138
  ```
127
139
 
128
140
  A project is the nearest folder from where you run `skill` up that holds a `skills.lock`, a `Gemfile`, a `gems.rb`, a `.git`, or a `package.json`, or else an agent's folder or instructions (`.claude`, `.agents`, `.cursor`, `AGENTS.md`, `CLAUDE.md`, and the like). Your home folder never counts, since agents keep their global folders there. `list`, `update`, and `remove` decide the same way.
@@ -163,7 +175,28 @@ chat = RubyLLM.chat.with_skills(:pdf)
163
175
  }
164
176
  ```
165
177
 
166
- Entries are sorted and carry no timestamps, so two branches adding different skills merge cleanly. The global lockfile lives in `~/.local/state/skill/skills.lock` (or `$XDG_STATE_HOME/skill`).
178
+ Entries are sorted and carry no timestamps, so two branches adding different skills merge cleanly.
179
+
180
+ The global lockfile lives in `~/.config/skill/skills.lock` (or `$XDG_CONFIG_HOME/skill`), so you can keep it with your dotfiles, like a Brewfile. skill 0.1 kept it in `~/.local/state/skill`; the first command that reads it moves it and says so.
181
+
182
+ ## Installing from the lockfile
183
+
184
+ `install` installs what `skills.lock` records, the way `bundle install` restores a `Gemfile.lock`: each skill at its recorded commit, into its recorded folders, creating any that are missing.
185
+
186
+ ```sh
187
+ git clone https://github.com/acme/app && cd app
188
+ skill install # the project's skills
189
+ skill install --global # yours, on a new computer, from ~/.config/skill/skills.lock
190
+ ```
191
+
192
+ ```
193
+ ✓ Installed pdf → app/skills/pdf, .claude/skills/pdf
194
+ · xlsx already installed
195
+ ```
196
+
197
+ A skill already in place with its recorded digest is left alone. Each copy it downloads must match that digest, or nothing is installed. A copy you edited is replaced only with `--force`. `install` never moves a skill to a newer commit: that is `update`'s job.
198
+
199
+ With a source, `skill install owner/repo` works like `skill add`, as `npx skills install` does.
167
200
 
168
201
  ## Updating
169
202
 
@@ -198,7 +231,7 @@ To move to another ref, add the skill again with it: `skill add anthropics/skill
198
231
  skill list # installed skills, and any that were edited or are missing
199
232
  skill list --json
200
233
  skill remove pdf # from every folder it was installed into
201
- skill remove pdf --agent claude-code
234
+ skill remove pdf --agents claude-code
202
235
  ```
203
236
 
204
237
  `list` also names skill folders `skill` did not install. `remove` deletes only what `skill` installed.
@@ -237,7 +270,7 @@ A skill's instructions steer a model and its scripts run with whatever your sand
237
270
  * **Installs are atomic.** Copies are built beside their folders and swapped in once all are ready. A failure leaves everything as it was.
238
271
  * **Skills stay where they belong.** `add`, `update`, and `remove` write only inside the project, or when global inside your home folder (and any agent folder you moved elsewhere with its environment variable). Folders are checked with `..` and symbolic links resolved, so a `skills.lock` entry, a `--dir`, or a linked folder that leads elsewhere is refused with an error naming it, and nothing changes.
239
272
  * **Your edits are yours.** A skill edited since it was installed, or a folder `skill` did not install, is replaced or removed only with `--force`.
240
- * **No downloads at run time.** `skill` reaches the network only in `add`, `update`, and `find`. What it installs is plain files in your project.
273
+ * **No downloads at run time.** `skill` reaches the network only in `add`, `install`, `update`, and `find`. What it installs is plain files in your project.
241
274
  * **GitHub without its API.** Refs come from git's own HTTP endpoint and files from one archive of the exact commit, so there is no rate limit and no git needed. A token, when set, goes only to GitHub.
242
275
 
243
276
  ## Environment
@@ -245,8 +278,8 @@ A skill's instructions steer a model and its scripts run with whatever your sand
245
278
  | Variable | Use |
246
279
  | --- | --- |
247
280
  | `GITHUB_TOKEN`, `GH_TOKEN` | Read private repositories |
248
- | `XDG_CONFIG_HOME`, `CLAUDE_CONFIG_DIR`, `CODEX_HOME` | Where agents keep their global folders |
249
- | `XDG_STATE_HOME` | Where the global lockfile lives |
281
+ | `CLAUDE_CONFIG_DIR`, `CODEX_HOME` | Where those agents keep their global folders |
282
+ | `XDG_CONFIG_HOME` | Where the global lockfile lives, and where some agents keep their global folders |
250
283
  | `INSTALL_INTERNAL_SKILLS` | Include skills whose front matter sets `metadata.internal: true` in `--all` |
251
284
  | `SKILLS_API_URL` | Another server with the skills.sh search endpoint |
252
285
  | `SKILL_MAX_*` | The limits above |
data/lib/skill/cli.rb CHANGED
@@ -6,7 +6,8 @@ module Skill
6
6
  # The +skill+ command line.
7
7
  class CLI
8
8
  COMMANDS = {
9
- 'add' => :Add, 'install' => :Add, 'a' => :Add, 'i' => :Add,
9
+ 'add' => :Add, 'a' => :Add,
10
+ 'install' => :Install, 'i' => :Install,
10
11
  'list' => :List, 'ls' => :List,
11
12
  'update' => :Update, 'upgrade' => :Update, 'up' => :Update,
12
13
  'remove' => :Remove, 'rm' => :Remove, 'uninstall' => :Remove,
@@ -19,6 +20,7 @@ module Skill
19
20
 
20
21
  Commands:
21
22
  add <source> Install skills from GitHub or a local folder
23
+ install Install what skills.lock records, at its commits
22
24
  list List installed skills
23
25
  update [names] Update installed skills, showing what changes first
24
26
  remove <names> Remove installed skills
@@ -17,13 +17,13 @@ module Skill
17
17
  attr_reader :session, :options
18
18
 
19
19
  # Parses the arguments with the options +define+ adds, plus the scope
20
- # options (+--global+, +--project+, +--agent+, +--dir+) unless +scope+
21
- # is false.
20
+ # options (+--global+, +--project+, +--agents+, +--dir+) unless +scope+
21
+ # is false, or only +--global+ and +--project+ when it is +:place+.
22
22
  # Returns the arguments left.
23
23
  def parse(banner, scope: true, &define)
24
24
  @parser = OptionParser.new do |opts|
25
25
  opts.banner = "Usage: skill #{banner}"
26
- scope_options(opts) if scope
26
+ scope_options(opts, targets: scope != :place) if scope
27
27
  define&.call(opts)
28
28
  end
29
29
  @parser.parse(@argv)
@@ -33,12 +33,16 @@ module Skill
33
33
  @parser.to_s
34
34
  end
35
35
 
36
- def scope_options(opts)
36
+ def scope_options(opts, targets: true)
37
37
  opts.on('-g', '--global', 'Use the home folder, even inside a project') { options[:global] = true }
38
38
  opts.on('-p', '--project', 'Use the current folder as a project, even outside one') do
39
39
  options[:global] = false
40
40
  end
41
- opts.on('-a', '--agent NAME', 'Target this agent (repeatable, comma-separated, * for all)') do |value|
41
+ return unless targets
42
+
43
+ opts.on('-a', '--agents NAMES', '--agent NAMES',
44
+ 'Target these agents (comma-separated, repeatable): all (detected, the default),',
45
+ 'coding (detected coding agents, not RubyLLM), or names from `skill agents`') do |value|
42
46
  options[:agents].concat(split(value))
43
47
  end
44
48
  opts.on('-d', '--dir PATH', 'Target this folder (repeatable)') do |value|
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skill
4
+ module Commands
5
+ # skill install: installs what the lockfile records, each skill at its
6
+ # recorded commit, into its recorded folders, the way +bundle install+
7
+ # restores a Gemfile.lock. Skills already in place with the recorded
8
+ # digest are left alone, and each copy fetched is checked against that
9
+ # digest before it is written. Given a source, works as +skill add+.
10
+ class Install < Command
11
+ def run
12
+ return Add.new(session, @argv).run if @argv.any? { |argument| !argument.start_with?('-') }
13
+
14
+ names = parse('install [options]', scope: :place) do |opts|
15
+ opts.on('-f', '--force', 'Replace skills that were edited since they were installed') do
16
+ options[:force] = true
17
+ end
18
+ end
19
+ raise UsageError, "skill install takes no names.\n\n#{usage}" unless names.empty?
20
+
21
+ install
22
+ end
23
+
24
+ private
25
+
26
+ def install
27
+ lock = workspace.lock
28
+ return say("No skills recorded in #{workspace.display(lock.path)}.") if lock.names.empty?
29
+
30
+ missing = lock.names.to_h { |name| [name, missing_folders(name)] }.reject { |_name, folders| folders.empty? }
31
+ (lock.names - missing.keys).each do |name|
32
+ say "#{style.dim('·')} #{style.bold(name)} #{style.dim('already installed')}"
33
+ end
34
+ copies = missing.group_by { |name, _folders| lock[name].values_at('source', 'commit') }.values
35
+ restore(copies.flat_map { |group| fetch(group) })
36
+ end
37
+
38
+ def restore(copies)
39
+ return 0 if copies.empty?
40
+
41
+ Installer.new.install(copies)
42
+ copies.each { |copy| say done("Installed #{style.bold(copy.name)} → #{copy.labels.join(', ')}") }
43
+ 0
44
+ end
45
+
46
+ # Returns the recorded folders of +name+ that do not hold it as
47
+ # recorded, after refusing any that hold an edited copy.
48
+ def missing_folders(name)
49
+ entry = workspace.lock[name]
50
+ workspace.folders(entry).reject do |folder|
51
+ path = Installer.destination(folder, name)
52
+ next true if workspace.unchanged?(path, entry)
53
+
54
+ workspace.ensure_replaceable(name, folder, options[:force])
55
+ false
56
+ end
57
+ end
58
+
59
+ # Returns the copies for +group+, pairs of a skill name and its
60
+ # folders, which share a source and a commit.
61
+ def fetch(group)
62
+ entry = workspace.lock[group.first.first]
63
+ checkout = session.fetcher.checkout(source(entry), resolution: resolution(entry))
64
+ group.map { |name, folders| copy(checkout, name, folders) }
65
+ end
66
+
67
+ def source(entry)
68
+ workspace.source_of(entry)
69
+ end
70
+
71
+ def resolution(entry)
72
+ commit = entry['commit']
73
+ return if commit.nil? || entry['ref_type'] == 'local'
74
+
75
+ unless commit.match?(/\A\h{40}\z/)
76
+ raise Error, "#{workspace.display(workspace.lock.path)} records an invalid commit for #{entry['source']}"
77
+ end
78
+
79
+ GitHub::Resolution.new(type: entry['ref_type'] == 'commit' ? :commit : entry['ref_type'].to_sym,
80
+ name: entry['ref'], commit: commit.downcase, branch: entry['ref'])
81
+ end
82
+
83
+ def copy(checkout, name, folders)
84
+ entry = workspace.lock[name]
85
+ path = File.join(checkout.root, entry['path'].to_s)
86
+ raise NotFoundError, "#{name}: #{entry['path']} is gone from #{entry['source']}" unless File.directory?(path)
87
+ if checkout.link_inside?(entry['path'])
88
+ raise UnsafeError, "#{entry['path']} holds a symbolic link; skills may not hold links"
89
+ end
90
+
91
+ executable = entry['executable'] ? true : false
92
+ tree = Tree.read(path, limits: session.limits)
93
+ verify(name, entry, tree.digest(executable: executable))
94
+ Installer::Copy.new(name: name, tree: tree, targets: folders, executable: executable, entry: entry,
95
+ labels: folders.map { |folder| workspace.display(File.join(folder, name)) })
96
+ end
97
+
98
+ def verify(name, entry, digest)
99
+ return if digest == entry['digest']
100
+
101
+ if entry['ref_type'] == 'local'
102
+ raise ConflictError, "#{name}: #{entry['source']} changed since it was recorded. " \
103
+ 'Run `skill update` to install what it holds now'
104
+ end
105
+ raise UnsafeError, "#{name}: #{entry['source']} at #{entry['commit'].to_s[0, 12]} does not match " \
106
+ "the digest #{workspace.display(workspace.lock.path)} records"
107
+ end
108
+ end
109
+ end
110
+ end
@@ -3,7 +3,7 @@
3
3
  module Skill
4
4
  module Commands
5
5
  # skill remove <names>: deletes skills this command installed, from
6
- # every folder it recorded, or only those +--agent+ and +--dir+ name.
6
+ # every folder it recorded, or only those +--agents+ and +--dir+ name.
7
7
  class Remove < Command
8
8
  def run
9
9
  names = parse('remove <names> [options]') do |opts|
@@ -7,6 +7,7 @@ module Skill
7
7
  autoload :Agents, 'skill/commands/agents'
8
8
  autoload :Command, 'skill/commands/command'
9
9
  autoload :Find, 'skill/commands/find'
10
+ autoload :Install, 'skill/commands/install'
10
11
  autoload :List, 'skill/commands/list'
11
12
  autoload :Remove, 'skill/commands/remove'
12
13
  autoload :Selection, 'skill/commands/selection'
@@ -8,7 +8,8 @@ module Skill
8
8
  # fetch it again and +remove+ knows what it owns. Entries are sorted by
9
9
  # name and carry no timestamps, so two branches adding different skills
10
10
  # merge cleanly. A project's lockfile sits at its root, to be committed;
11
- # the global one in +$XDG_STATE_HOME/skill+ (+~/.local/state/skill+).
11
+ # the global one in +$XDG_CONFIG_HOME/skill+ (+~/.config/skill+), beside
12
+ # other settings people keep with their dotfiles.
12
13
  #
13
14
  # {
14
15
  # "version": 1,
@@ -35,10 +36,36 @@ module Skill
35
36
 
36
37
  # Returns the global lockfile's path.
37
38
  def self.global_path(env:, home:)
38
- state = env['XDG_STATE_HOME'].to_s.strip
39
- File.join(state.empty? ? File.join(home, '.local/state') : state, 'skill', NAME)
39
+ File.join(xdg(env, 'XDG_CONFIG_HOME', File.join(home, '.config')), 'skill', NAME)
40
40
  end
41
41
 
42
+ # Returns where skill 0.1 kept the global lockfile.
43
+ def self.legacy_global_path(env:, home:)
44
+ File.join(xdg(env, 'XDG_STATE_HOME', File.join(home, '.local/state')), 'skill', NAME)
45
+ end
46
+
47
+ # Moves the global lockfile from where skill 0.1 kept it, when only that
48
+ # one exists. Returns the old path when it moved it, else +nil+.
49
+ def self.migrate_global(env:, home:)
50
+ old = legacy_global_path(env: env, home: home)
51
+ current = global_path(env: env, home: home)
52
+ return unless File.file?(old) && !File.exist?(current)
53
+
54
+ FileUtils.mkdir_p(File.dirname(current))
55
+ File.rename(old, current)
56
+ old
57
+ rescue SystemCallError
58
+ FileUtils.cp(old, current)
59
+ FileUtils.rm_f(old)
60
+ old
61
+ end
62
+
63
+ def self.xdg(env, variable, default)
64
+ value = env[variable].to_s.strip
65
+ value.empty? ? default : value
66
+ end
67
+ private_class_method :xdg
68
+
42
69
  def initialize(path)
43
70
  @path = path
44
71
  @skills = File.exist?(path) ? parse(File.read(path, encoding: 'UTF-8')) : {}
data/lib/skill/session.rb CHANGED
@@ -53,7 +53,11 @@ module Skill
53
53
  # Returns the Workspace for the project, or the home folder with
54
54
  # +global+, or with +global+ +nil+ whichever +cwd+ is in.
55
55
  def workspace(global: nil)
56
- @workspace ||= Workspace.open(cwd: cwd, home: home, env: env, global: global, limits: limits)
56
+ @workspace ||= Workspace.open(cwd: cwd, home: home, env: env, global: global, limits: limits).tap do |space|
57
+ if space.moved_lock
58
+ warn("Moved the global lockfile from #{space.display(space.moved_lock)} to #{space.display(space.lock.path)}")
59
+ end
60
+ end
57
61
  end
58
62
 
59
63
  def limits
data/lib/skill/targets.rb CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  module Skill
4
4
  # Decides which folders a command installs to, lists, or updates: the
5
- # folders of the agents named with +--agent+, the folders named with
6
- # +--dir+, or else the ones the project or home folder shows signs of.
5
+ # folders of the agents and groups named with +--agents+, the folders
6
+ # named with +--dir+, or else the ones the project or home folder shows
7
+ # signs of.
7
8
  class Targets
8
9
  # One folder skills are installed into, and the agents that read it.
9
10
  Target = Struct.new(:path, :label, :agents, keyword_init: true)
@@ -30,6 +31,14 @@ module Skill
30
31
 
31
32
  SHARED_GLOBAL_DIRS = %w[~/.agents/skills {config}/agents/skills].freeze
32
33
  FALLBACK = 'universal'
34
+ RUBY_LLM = 'rubyllm'
35
+
36
+ # Names +--agents+ takes for several agents at once: +all+, every agent
37
+ # detected (RubyLLM's +app/skills+ included in a RubyLLM project), and
38
+ # +coding+, every coding agent detected, never RubyLLM. Both fall back to
39
+ # the shared +.agents/skills+ folder when they detect none. +*+ is every
40
+ # agent +skill agents+ lists.
41
+ GROUPS = %w[all coding *].freeze
33
42
 
34
43
  attr_reader :root, :home, :env
35
44
 
@@ -45,11 +54,12 @@ module Skill
45
54
  @global
46
55
  end
47
56
 
48
- # Returns the Targets for +agents+ and +dirs+, or the detected ones when
49
- # both are empty. Folders several agents share are returned once.
57
+ # Returns the Targets for +agents+ (names, aliases, or GROUPS) and
58
+ # +dirs+, or the detected ones (the +all+ group) when both are empty.
59
+ # Folders several agents share are returned once.
50
60
  def resolve(agents: [], dirs: [])
51
- chosen = agents_named(agents)
52
- list = chosen.empty? && dirs.empty? ? detected : chosen.filter_map { |agent| target_for(agent, strict: true) }
61
+ agents = ['all'] if agents.empty? && dirs.empty?
62
+ list = agents_named(agents).filter_map { |agent| target_for(agent, strict: true) }
53
63
  list += dirs.map { |dir| Target.new(path: File.expand_path(dir, base), label: dir, agents: []) }
54
64
  merge(list)
55
65
  end
@@ -60,9 +70,11 @@ module Skill
60
70
  end
61
71
 
62
72
  # Returns the agents the project or home folder shows signs of, or the
63
- # shared +.agents/skills+ folder when it shows none.
64
- def detected_agents
73
+ # shared +.agents/skills+ folder when it shows none. With +coding+,
74
+ # leaves RubyLLM out.
75
+ def detected_agents(coding: false)
65
76
  found = Agents.all.select { |agent| global? ? installed?(agent) : used_in_project?(agent) }
77
+ found.reject! { |agent| agent.key == RUBY_LLM } if coding
66
78
  found.empty? ? [Agents.fetch(FALLBACK)] : found
67
79
  end
68
80
 
@@ -103,13 +115,14 @@ module Skill
103
115
  end
104
116
 
105
117
  def agents_named(names)
106
- return Agents.all.select { |agent| global? ? agent.global? : true } if names.include?('*')
107
-
108
- names.map { |name| Agents.fetch(name) }.uniq
109
- end
110
-
111
- def detected
112
- detected_agents.filter_map { |agent| target_for(agent) }
118
+ names.flat_map do |name|
119
+ case name.to_s.strip.downcase
120
+ when '*' then Agents.all.select { |agent| global? ? agent.global? : true }
121
+ when 'all' then detected_agents
122
+ when 'coding' then detected_agents(coding: true)
123
+ else [Agents.fetch(name)]
124
+ end
125
+ end.uniq
113
126
  end
114
127
 
115
128
  def target_for(agent, strict: false)
@@ -129,7 +142,7 @@ module Skill
129
142
  end
130
143
 
131
144
  def used_in_project?(agent)
132
- return ruby_llm? if agent.key == 'rubyllm'
145
+ return ruby_llm? if agent.key == RUBY_LLM
133
146
  return File.directory?(File.join(root, agent.project_dir)) if agent.key == FALLBACK
134
147
 
135
148
  (PROJECT_SIGNS.fetch(agent.key, []) + own_signs(agent)).any? { |sign| File.exist?(File.join(root, sign)) }
data/lib/skill/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skill
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
@@ -8,7 +8,7 @@ module Skill
8
8
  ROOT_MARKERS = %w[skills.lock Gemfile gems.rb .git package.json].freeze
9
9
  AGENT_MARKERS = %w[.agents .claude .codex .cursor .gemini .windsurf AGENTS.md CLAUDE.md GEMINI.md].freeze
10
10
 
11
- attr_reader :root, :targets, :lock
11
+ attr_reader :root, :targets, :lock, :moved_lock
12
12
 
13
13
  # Opens the project around +cwd+, or the home folder with +global+.
14
14
  # With +global+ +nil+, uses the home folder when +cwd+ is in no
@@ -17,9 +17,10 @@ module Skill
17
17
  project = project_root(cwd, home)
18
18
  global = project.nil? if global.nil?
19
19
  root = global ? home : project || cwd
20
+ moved = Lockfile.migrate_global(env: env, home: home) if global
20
21
  lock = global ? Lockfile.global_path(env: env, home: home) : File.join(root, Lockfile::NAME)
21
22
  new(root: root, targets: Targets.new(root: root, home: home, env: env, global: global),
22
- lock: Lockfile.new(lock), limits: limits)
23
+ lock: Lockfile.new(lock), limits: limits, moved_lock: moved)
23
24
  end
24
25
 
25
26
  # Returns the nearest folder from +cwd+ up holding a lockfile, a
@@ -47,7 +48,10 @@ module Skill
47
48
  targets.global?
48
49
  end
49
50
 
50
- def initialize(root:, targets:, lock:, limits:)
51
+ # +moved_lock+ is where the global lockfile was before this run moved
52
+ # it, if it did.
53
+ def initialize(root:, targets:, lock:, limits:, moved_lock: nil)
54
+ @moved_lock = moved_lock
51
55
  @root = root
52
56
  @targets = targets
53
57
  @lock = lock
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skill
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
@@ -35,6 +35,7 @@ files:
35
35
  - lib/skill/commands/agents.rb
36
36
  - lib/skill/commands/command.rb
37
37
  - lib/skill/commands/find.rb
38
+ - lib/skill/commands/install.rb
38
39
  - lib/skill/commands/list.rb
39
40
  - lib/skill/commands/remove.rb
40
41
  - lib/skill/commands/selection.rb