shai-cli 0.2.0 → 0.3.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: 1ea758284c9ecb5985d8789033ee4368ec95757ac056a0b5870f9d8f523d9282
4
- data.tar.gz: 046a439279ef5a6c2cf32eeb9f5d00f7656887cb310168c334306796cd73f713
3
+ metadata.gz: '00490a68be250015b1cd43dd40cc804d79e82bb74ddc6fc65a4ef90d27b7de69'
4
+ data.tar.gz: d694b3f181f571b3ac8ebbbe415c92c805c4fb780cd8f3dae4cce0f6b32e6a6b
5
5
  SHA512:
6
- metadata.gz: 3f158f9131a3d10a7717b4d2109e9ae37682b4c2d2f68de5ab516c0c4e789ad86556dff17b973c7a8daa23527486064057c1ef1b72c0064d206141a3f87bc204
7
- data.tar.gz: e266d13cb6135d0530f5363e1f568a57fb2558a0a0b747b1b2820c4e921247d5114624727f4865711f44758d619f22bd233cd6464326612f2944aaba1b2514cc
6
+ metadata.gz: '08980b4e9a5623c01215cdbe58b1ad8d2ade01cd07ca4d84a3c61db346cbf0513efd663037740570fd17c8c2e61e14b7597d1444bd6701afad6a595ab2f95763'
7
+ data.tar.gz: 94016712713d1f3731b82b39a75e0d4d23b37f4149a2503bcfe8ae09aabf1ad59f03be7107f1dad0b1177e2168981fc07a5ab3ceaff123822adf1192220be7f0
data/README.md CHANGED
@@ -56,8 +56,8 @@ shai login
56
56
  # Search for public configurations
57
57
  shai search "claude code"
58
58
 
59
- # Install a configuration to your project
60
- shai install anthropic/claude-expert
59
+ # Install a configuration (--global for ~/, --local for ./)
60
+ shai install anthropic/claude-expert --local
61
61
 
62
62
  # Create and share your own configuration
63
63
  shai init
@@ -76,12 +76,12 @@ These commands work without authentication for public configurations:
76
76
  | ------------------------- | ------------------------------------------------------ |
77
77
  | `shai search <query>` | Search public configurations |
78
78
  | `shai install <config>` | Install a public configuration (use `owner/slug` format) |
79
- | `shai uninstall <config>` | Uninstall a public configuration |
79
+ | `shai uninstall <config>` | Uninstall a configuration |
80
80
 
81
81
  ```bash
82
82
  # No login needed for public configs
83
83
  shai search "claude code"
84
- shai install anthropic/claude-expert
84
+ shai install anthropic/claude-expert --local
85
85
  shai uninstall anthropic/claude-expert
86
86
  ```
87
87
 
@@ -151,21 +151,69 @@ Token expires: March 11, 2026
151
151
 
152
152
  ### Using Configurations
153
153
 
154
- | Command | Description |
155
- | ------------------------- | ---------------------------------------- |
156
- | `shai install <config>` | Install a configuration to local project |
157
- | `shai uninstall <config>` | Remove an installed configuration |
154
+ | Command | Description |
155
+ | -------------------------------------- | ---------------------------------------- |
156
+ | `shai install <config>` | Install a configuration (prompts for location) |
157
+ | `shai install <config> --global` | Install to home directory (`~/`) |
158
+ | `shai install <config> --local` | Install to current directory (`./`) |
159
+ | `shai install <config> --path <dir>` | Install to a specific directory |
160
+ | `shai uninstall <config>` | Remove an installed configuration |
161
+ | `shai uninstall <config> --global` | Uninstall from home directory |
162
+ | `shai uninstall <config> --local` | Uninstall from current directory |
163
+
164
+ **Global vs Local installs:**
165
+
166
+ Configurations can be installed globally (to `~/`) or locally (to `./`). Global installs apply across all projects — useful for AI agent config files like `CLAUDE.md` or `.cursorrules`. Local installs are project-specific.
167
+
168
+ ```bash
169
+ # Install globally (applies everywhere)
170
+ $ shai install anthropic/claude-expert --global
171
+
172
+ # Install locally (current project only)
173
+ $ shai install anthropic/claude-expert --local
174
+
175
+ # If you don't specify, shai will ask
176
+ $ shai install anthropic/claude-expert
177
+ ? Where do you want to install?
178
+ ./ (local - current directory)
179
+ ~/ (global - home directory)
180
+ ```
181
+
182
+ **Uninstalling:**
183
+
184
+ shai tracks where each configuration was installed, so uninstall works without needing to remember the path:
158
185
 
159
186
  ```bash
160
187
  $ shai uninstall anthropic/claude-expert
161
- [✔] Fetching anthropic/claude-expert...
162
- Remove 3 files and 1 folder from 'anthropic/claude-expert'? (y/N) y
188
+ Remove 3 files from 'anthropic/claude-expert'? (y/N) y
163
189
 
164
190
  ✓ Uninstalled anthropic/claude-expert
165
191
  ```
166
192
 
167
193
  ---
168
194
 
195
+ ### Skills
196
+
197
+ | Command | Description |
198
+ | ------------------------------------------------ | ---------------------------------------- |
199
+ | `shai skills` | List all AI agent skills and their status |
200
+ | `shai skills enable <name>` | Enable a disabled skill |
201
+ | `shai skills enable <name> --global` | Enable a global skill |
202
+ | `shai skills disable <name>` | Disable an enabled skill |
203
+ | `shai skills disable <name> --local` | Disable a local skill |
204
+ | `shai skills disable <name> --agent codex` | Target a specific agent |
205
+
206
+ Skills are `SKILL.md` files discovered across multiple AI agent directories:
207
+
208
+ | Agent | Global path | Local path |
209
+ | ------ | ------------------------------ | ----------------------------- |
210
+ | Claude | `~/.claude/skills/*/SKILL.md` | `./.claude/skills/*/SKILL.md` |
211
+ | Codex | `~/.agents/skills/*/SKILL.md` | `./.agents/skills/*/SKILL.md` |
212
+
213
+ Disabling a skill renames `SKILL.md` to `SKILL.md.disabled` so the AI tool no longer loads it. Re-enabling reverses the rename. Use `--agent` to target a specific agent when the same skill name exists in multiple agents.
214
+
215
+ ---
216
+
169
217
  ### Authoring Configurations
170
218
 
171
219
  | Command | Description |
@@ -218,6 +266,12 @@ exclude:
218
266
  ## Examples
219
267
 
220
268
  ```bash
269
+ # Install globally (to ~/)
270
+ shai install anthropic/claude-expert --global
271
+
272
+ # Install locally (to ./)
273
+ shai install anthropic/claude-expert --local
274
+
221
275
  # Install to a specific directory
222
276
  shai install anthropic/claude-expert --path ./my-project
223
277
 
@@ -226,6 +280,9 @@ shai install anthropic/claude-expert --dry-run
226
280
 
227
281
  # Force overwrite existing files
228
282
  shai install anthropic/claude-expert --force
283
+
284
+ # Uninstall (auto-detects where it was installed)
285
+ shai uninstall anthropic/claude-expert
229
286
  ```
230
287
 
231
288
  ---
data/lib/shai/cli.rb CHANGED
@@ -5,6 +5,7 @@ require_relative "commands/auth"
5
5
  require_relative "commands/configurations"
6
6
  require_relative "commands/sync"
7
7
  require_relative "commands/config"
8
+ require_relative "commands/skills"
8
9
  require_relative "ui"
9
10
 
10
11
  module Shai
@@ -13,6 +14,7 @@ module Shai
13
14
  include Commands::Configurations
14
15
  include Commands::Sync
15
16
  include Commands::Config
17
+ include Commands::Skills
16
18
 
17
19
  def self.exit_on_failure?
18
20
  true
@@ -36,10 +38,15 @@ module Shai
36
38
  shell.say " search <query> Search public configurations"
37
39
  shell.say " open <config> Open a configuration in the browser"
38
40
  shell.say ""
39
- shell.say "USING CONFIGURATIONS (install to current project):"
40
- shell.say " install <config> Install a configuration to local project"
41
+ shell.say "USING CONFIGURATIONS:"
42
+ shell.say " install <config> Install a configuration (--global, --local, or --path)"
41
43
  shell.say " uninstall <config> Remove an installed configuration"
42
44
  shell.say ""
45
+ shell.say "SKILLS:"
46
+ shell.say " skills List all AI agent skills and their status"
47
+ shell.say " skills enable <n> Enable a disabled skill (--global, --local, --agent)"
48
+ shell.say " skills disable <n> Disable an enabled skill (--global, --local, --agent)"
49
+ shell.say ""
43
50
  shell.say "AUTHORING CONFIGURATIONS (create and publish):"
44
51
  shell.say " init Initialize a new configuration"
45
52
  shell.say " push Push local changes to remote"
@@ -71,16 +71,30 @@ module Shai
71
71
  end
72
72
  end
73
73
 
74
- desc "install CONFIGURATION", "Install a configuration to local project"
74
+ desc "install CONFIGURATION", "Install a configuration"
75
75
  option :force, type: :boolean, aliases: "-f", default: false, desc: "Overwrite existing files"
76
76
  option :dry_run, type: :boolean, default: false, desc: "Show what would be installed"
77
- option :path, type: :string, default: ".", desc: "Install to specific directory"
77
+ option :path, type: :string, desc: "Install to specific directory"
78
+ option :global, type: :boolean, default: false, desc: "Install to home directory (~)"
79
+ option :local, type: :boolean, default: false, desc: "Install to current directory (./)"
78
80
  def install(configuration)
79
81
  owner, slug = parse_configuration_name(configuration)
80
82
  display_name = owner ? "#{owner}/#{slug}" : slug
81
- base_path = File.expand_path(options[:path])
83
+ base_path = resolve_install_path
82
84
  shairc_path = File.join(base_path, ".shairc")
83
85
 
86
+ registry = InstallRegistry.new
87
+
88
+ # Check if already installed elsewhere
89
+ if registry.has?(display_name) && !options[:force]
90
+ existing_path = registry.path_for(display_name)
91
+ if File.expand_path(existing_path) != base_path
92
+ ui.error("'#{display_name}' is already installed at #{existing_path}")
93
+ ui.info("Run `shai uninstall #{display_name}` first, or use --force to reinstall.")
94
+ exit EXIT_INVALID_INPUT
95
+ end
96
+ end
97
+
84
98
  installed = InstalledProjects.new(base_path)
85
99
 
86
100
  # Check if this exact project is already installed
@@ -214,6 +228,7 @@ module Shai
214
228
 
215
229
  # Track the installed project
216
230
  installed.add_project(display_name, created_files)
231
+ registry.add(display_name, base_path)
217
232
 
218
233
  # Record the install for analytics (fire and forget)
219
234
  begin
@@ -301,17 +316,21 @@ module Shai
301
316
  end
302
317
  end
303
318
 
304
- desc "uninstall [CONFIGURATION]", "Remove an installed configuration from local project"
319
+ desc "uninstall [CONFIGURATION]", "Remove an installed configuration"
305
320
  option :dry_run, type: :boolean, default: false, desc: "Show what would be removed"
306
- option :path, type: :string, default: ".", desc: "Path where configuration is installed"
321
+ option :global, type: :boolean, default: false, desc: "Uninstall from home directory (~)"
322
+ option :local, type: :boolean, default: false, desc: "Uninstall from current directory (./)"
307
323
  def uninstall(configuration = nil)
308
- base_path = File.expand_path(options[:path])
324
+ registry = InstallRegistry.new
325
+
326
+ # Resolve base_path: explicit flag, registry lookup, or fallback to "."
327
+ base_path = resolve_uninstall_path(configuration, registry)
309
328
  installed = InstalledProjects.new(base_path)
310
329
 
311
330
  # If no configuration specified, determine which to uninstall
312
331
  if configuration.nil?
313
332
  if installed.empty?
314
- ui.error("No configurations installed in this directory.")
333
+ ui.error("No configurations installed in #{base_path}")
315
334
  ui.info("Usage: shai uninstall <configuration>")
316
335
  exit EXIT_INVALID_INPUT
317
336
  elsif installed.project_count == 1
@@ -420,6 +439,7 @@ module Shai
420
439
 
421
440
  # Update tracking
422
441
  installed.remove_project(display_name)
442
+ registry.remove(display_name)
423
443
 
424
444
  # Remove tracking file if no more projects
425
445
  if installed.empty?
@@ -439,6 +459,56 @@ module Shai
439
459
 
440
460
  private
441
461
 
462
+ def resolve_uninstall_path(configuration, registry)
463
+ if options[:global] && options[:local]
464
+ ui.error("Conflicting options: use only one of --global or --local.")
465
+ exit EXIT_INVALID_INPUT
466
+ end
467
+
468
+ if options[:global]
469
+ return File.expand_path(Dir.home)
470
+ elsif options[:local]
471
+ return File.expand_path(".")
472
+ end
473
+
474
+ # Try registry lookup if a configuration name is given
475
+ if configuration
476
+ owner, slug = parse_configuration_name(configuration)
477
+ display_name = owner ? "#{owner}/#{slug}" : slug
478
+ registered_path = registry.path_for(display_name)
479
+ return File.expand_path(registered_path) if registered_path
480
+ end
481
+
482
+ # Default to current directory
483
+ File.expand_path(".")
484
+ end
485
+
486
+ def resolve_install_path
487
+ flag_count = 0
488
+ flag_count += 1 if options[:global]
489
+ flag_count += 1 if options[:local]
490
+ flag_count += 1 if options[:path]
491
+
492
+ if flag_count > 1
493
+ ui.error("Conflicting options: use only one of --global, --local, or --path.")
494
+ exit EXIT_INVALID_INPUT
495
+ end
496
+
497
+ if options[:path]
498
+ File.expand_path(options[:path])
499
+ elsif options[:global]
500
+ File.expand_path(Dir.home)
501
+ elsif options[:local]
502
+ File.expand_path(".")
503
+ else
504
+ choice = ui.select("Where do you want to install?", [
505
+ {name: "./ (local - current directory)", value: :local},
506
+ {name: "~/ (global - home directory)", value: :global}
507
+ ])
508
+ (choice == :global) ? File.expand_path(Dir.home) : File.expand_path(".")
509
+ end
510
+ end
511
+
442
512
  def parse_configuration_name(name)
443
513
  if name.include?("/")
444
514
  name.split("/", 2)
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shai
4
+ module Commands
5
+ module Skills
6
+ def self.included(base)
7
+ base.class_eval do
8
+ desc "skills [SUBCOMMAND]", "Manage AI agent skills"
9
+ method_option :global, type: :boolean, default: false, desc: "Target global skills"
10
+ method_option :local, type: :boolean, default: false, desc: "Target local skills"
11
+ method_option :agent, type: :string, desc: "Target a specific agent (e.g. claude, codex)"
12
+ def skills(subcommand = nil, *args)
13
+ case subcommand
14
+ when nil, "list"
15
+ skills_list
16
+ when "enable"
17
+ name = args.first
18
+ unless name
19
+ ui.error("Usage: shai skills enable <name> [--global|--local] [--agent <agent>]")
20
+ exit EXIT_INVALID_INPUT
21
+ end
22
+ skills_enable(name)
23
+ when "disable"
24
+ name = args.first
25
+ unless name
26
+ ui.error("Usage: shai skills disable <name> [--global|--local] [--agent <agent>]")
27
+ exit EXIT_INVALID_INPUT
28
+ end
29
+ skills_disable(name)
30
+ else
31
+ ui.error("Unknown subcommand: #{subcommand}. Use `shai skills`, `shai skills enable`, or `shai skills disable`")
32
+ exit EXIT_INVALID_INPUT
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def skills_list
41
+ scanner = SkillScanner.new
42
+ all_skills = scanner.scan_all
43
+ populate_sources!(all_skills)
44
+
45
+ if all_skills.empty?
46
+ ui.info("No skills found.")
47
+ ui.blank
48
+ ui.info("Skill discovery paths:")
49
+ SkillScanner::AGENTS.each do |agent|
50
+ ui.info(" Global: ~/#{agent[:skill_dir]}/*/SKILL.md (#{agent[:name]})")
51
+ ui.info(" Local: ./#{agent[:skill_dir]}/*/SKILL.md (#{agent[:name]})")
52
+ end
53
+ return
54
+ end
55
+
56
+ global_skills = all_skills.select { |s| s.scope == :global }
57
+ local_skills = all_skills.select { |s| s.scope == :local }
58
+
59
+ if global_skills.any?
60
+ ui.blank
61
+ ui.header("Global skills")
62
+ ui.blank
63
+ ui.table(
64
+ ["Skill", "Status", "Agent", "Source"],
65
+ global_skills.map { |s| skill_row(s) }
66
+ )
67
+ end
68
+
69
+ if local_skills.any?
70
+ ui.blank
71
+ ui.header("Local skills")
72
+ ui.blank
73
+ ui.table(
74
+ ["Skill", "Status", "Agent", "Source"],
75
+ local_skills.map { |s| skill_row(s) }
76
+ )
77
+ end
78
+
79
+ enabled_count = all_skills.count(&:enabled)
80
+ disabled_count = all_skills.count { |s| !s.enabled }
81
+ ui.blank
82
+ ui.info("#{enabled_count} enabled, #{disabled_count} disabled")
83
+ end
84
+
85
+ def skills_enable(name)
86
+ scanner, skill = resolve_skill(scanner_instance, name)
87
+ return unless skill
88
+
89
+ if skill.enabled
90
+ ui.info("Skill '#{name}' is already enabled (#{skill.scope}, #{skill.agent})")
91
+ return
92
+ end
93
+
94
+ scanner.enable!(skill)
95
+ ui.success("Enabled skill '#{name}' (#{skill.scope}, #{skill.agent})")
96
+ end
97
+
98
+ def skills_disable(name)
99
+ scanner, skill = resolve_skill(scanner_instance, name)
100
+ return unless skill
101
+
102
+ unless skill.enabled
103
+ ui.info("Skill '#{name}' is already disabled (#{skill.scope}, #{skill.agent})")
104
+ return
105
+ end
106
+
107
+ scanner.disable!(skill)
108
+ ui.success("Disabled skill '#{name}' (#{skill.scope}, #{skill.agent})")
109
+ end
110
+
111
+ def scanner_instance
112
+ SkillScanner.new
113
+ end
114
+
115
+ def resolve_skill(scanner, name)
116
+ validate_scope_flags!
117
+ validate_agent_flag!
118
+
119
+ scope = if options[:global]
120
+ :global
121
+ elsif options[:local]
122
+ :local
123
+ end
124
+
125
+ agent = options[:agent]
126
+
127
+ matches = scanner.find(name, scope: scope, agent: agent)
128
+
129
+ if matches.empty?
130
+ ui.error("Skill '#{name}' not found. Run `shai skills` to see available skills.")
131
+ exit EXIT_NOT_FOUND
132
+ end
133
+
134
+ if matches.length > 1
135
+ ambiguous_by_scope = matches.map(&:scope).uniq.length > 1
136
+ ambiguous_by_agent = matches.map(&:agent).uniq.length > 1
137
+
138
+ ui.error("Skill '#{name}' exists in multiple #{ambiguous_by_agent ? "agents" : "scopes"}:")
139
+ matches.each { |s| ui.info(" - #{s.scope}, #{s.agent} (#{s.path})") }
140
+
141
+ hints = []
142
+ hints << "--global or --local" if ambiguous_by_scope
143
+ hints << "--agent <agent>" if ambiguous_by_agent
144
+ ui.info("Use #{hints.join(" and ")} to specify which one.")
145
+ exit EXIT_INVALID_INPUT
146
+ end
147
+
148
+ [scanner, matches.first]
149
+ end
150
+
151
+ def validate_scope_flags!
152
+ if options[:global] && options[:local]
153
+ ui.error("Cannot use --global and --local together")
154
+ exit EXIT_INVALID_INPUT
155
+ end
156
+ end
157
+
158
+ def validate_agent_flag!
159
+ agent = options[:agent]
160
+ return unless agent
161
+
162
+ known = SkillScanner::AGENTS.map { |a| a[:name] }
163
+ unless known.include?(agent)
164
+ ui.error("Unknown agent '#{agent}'. Known agents: #{known.join(", ")}")
165
+ exit EXIT_INVALID_INPUT
166
+ end
167
+ end
168
+
169
+ def skill_row(skill)
170
+ status = skill.enabled ? "enabled" : "disabled"
171
+ source = skill.source || "-"
172
+ [skill.name, status, skill.agent, source]
173
+ end
174
+
175
+ def populate_sources!(skills)
176
+ [Dir.home, Dir.pwd].each do |base|
177
+ installed = InstalledProjects.new(base)
178
+ next if installed.empty?
179
+
180
+ skills.each do |skill|
181
+ next if skill.source
182
+
183
+ installed.project_slugs.each do |slug|
184
+ files = installed.files_for_project(slug)
185
+ SkillScanner::AGENTS.each do |agent|
186
+ skill_relative = "#{agent[:skill_dir]}/#{skill.name}/SKILL.md"
187
+ if files.any? { |f| f == skill_relative || f.start_with?("#{agent[:skill_dir]}/#{skill.name}/") }
188
+ skill.source = slug
189
+ break
190
+ end
191
+ end
192
+ break if skill.source
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "time"
5
+ require "fileutils"
6
+
7
+ module Shai
8
+ class InstallRegistry
9
+ def initialize
10
+ @file_path = File.join(Shai.configuration.config_dir, "installations.yml")
11
+ @data = load_data
12
+ end
13
+
14
+ attr_reader :file_path
15
+
16
+ def add(slug, path)
17
+ @data["projects"][slug] = {
18
+ "path" => File.expand_path(path),
19
+ "installed_at" => Time.now.iso8601
20
+ }
21
+ save!
22
+ end
23
+
24
+ def remove(slug)
25
+ removed = @data["projects"].delete(slug)
26
+ save! if removed
27
+ removed
28
+ end
29
+
30
+ def path_for(slug)
31
+ @data.dig("projects", slug, "path")
32
+ end
33
+
34
+ def all
35
+ @data["projects"].dup
36
+ end
37
+
38
+ def has?(slug)
39
+ @data["projects"].key?(slug)
40
+ end
41
+
42
+ private
43
+
44
+ def load_data
45
+ return default_data unless File.exist?(@file_path)
46
+
47
+ raw = YAML.safe_load_file(@file_path) || {}
48
+ (raw.is_a?(Hash) && raw["projects"].is_a?(Hash)) ? raw : default_data
49
+ rescue
50
+ default_data
51
+ end
52
+
53
+ def default_data
54
+ {"projects" => {}}
55
+ end
56
+
57
+ def save!
58
+ FileUtils.mkdir_p(File.dirname(@file_path))
59
+ File.write(@file_path, YAML.dump(@data))
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shai
4
+ Skill = Struct.new(:name, :scope, :enabled, :path, :source, :agent, keyword_init: true)
5
+
6
+ class SkillScanner
7
+ SKILL_FILE = "SKILL.md"
8
+ DISABLED_SUFFIX = ".disabled"
9
+
10
+ AGENTS = [
11
+ {name: "claude", skill_dir: ".claude/skills"},
12
+ {name: "codex", skill_dir: ".agents/skills"}
13
+ ].freeze
14
+
15
+ attr_reader :local_base, :global_base
16
+
17
+ def initialize(local_base: Dir.pwd, global_base: Dir.home)
18
+ @local_base = File.expand_path(local_base)
19
+ @global_base = File.expand_path(global_base)
20
+ end
21
+
22
+ def scan_all
23
+ skills = []
24
+ skills.concat(scan_scope(:global))
25
+ skills.concat(scan_scope(:local))
26
+ skills
27
+ end
28
+
29
+ def find(name, scope: nil, agent: nil)
30
+ skills = scan_all
31
+ skills.select! { |s| s.name == name }
32
+ skills.select! { |s| s.scope == scope } if scope
33
+ skills.select! { |s| s.agent == agent } if agent
34
+ skills
35
+ end
36
+
37
+ def enable!(skill)
38
+ return false if skill.enabled
39
+ disabled_path = skill.path
40
+ enabled_path = disabled_path.sub(/#{Regexp.escape(DISABLED_SUFFIX)}$/o, "")
41
+ return false unless File.exist?(disabled_path)
42
+
43
+ File.rename(disabled_path, enabled_path)
44
+ true
45
+ end
46
+
47
+ def disable!(skill)
48
+ return false unless skill.enabled
49
+ enabled_path = skill.path
50
+ disabled_path = "#{enabled_path}#{DISABLED_SUFFIX}"
51
+ return false unless File.exist?(enabled_path)
52
+
53
+ File.rename(enabled_path, disabled_path)
54
+ true
55
+ end
56
+
57
+ private
58
+
59
+ def scan_scope(scope)
60
+ base = (scope == :global) ? global_base : local_base
61
+ skills = []
62
+
63
+ AGENTS.each do |agent|
64
+ skill_dir = File.join(base, agent[:skill_dir])
65
+ next unless Dir.exist?(skill_dir)
66
+
67
+ Dir.children(skill_dir).sort.each do |entry|
68
+ full_dir = File.join(skill_dir, entry)
69
+ next unless File.directory?(full_dir)
70
+
71
+ enabled_file = File.join(full_dir, SKILL_FILE)
72
+ disabled_file = File.join(full_dir, "#{SKILL_FILE}#{DISABLED_SUFFIX}")
73
+
74
+ if File.exist?(enabled_file)
75
+ skills << Skill.new(
76
+ name: entry,
77
+ scope: scope,
78
+ enabled: true,
79
+ path: enabled_file,
80
+ source: nil,
81
+ agent: agent[:name]
82
+ )
83
+ elsif File.exist?(disabled_file)
84
+ skills << Skill.new(
85
+ name: entry,
86
+ scope: scope,
87
+ enabled: false,
88
+ path: disabled_file,
89
+ source: nil,
90
+ agent: agent[:name]
91
+ )
92
+ end
93
+ end
94
+ end
95
+
96
+ skills
97
+ end
98
+ end
99
+ end
data/lib/shai/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Shai
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/shai.rb CHANGED
@@ -5,6 +5,8 @@ require_relative "shai/configuration"
5
5
  require_relative "shai/credentials"
6
6
  require_relative "shai/api_client"
7
7
  require_relative "shai/installed_projects"
8
+ require_relative "shai/install_registry"
9
+ require_relative "shai/skill_scanner"
8
10
  require_relative "shai/cli"
9
11
 
10
12
  module Shai
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shai-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Jimenez
@@ -139,10 +139,13 @@ files:
139
139
  - lib/shai/commands/auth.rb
140
140
  - lib/shai/commands/config.rb
141
141
  - lib/shai/commands/configurations.rb
142
+ - lib/shai/commands/skills.rb
142
143
  - lib/shai/commands/sync.rb
143
144
  - lib/shai/configuration.rb
144
145
  - lib/shai/credentials.rb
146
+ - lib/shai/install_registry.rb
145
147
  - lib/shai/installed_projects.rb
148
+ - lib/shai/skill_scanner.rb
146
149
  - lib/shai/ui.rb
147
150
  - lib/shai/version.rb
148
151
  homepage: https://shaicli.dev
@@ -167,7 +170,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
170
  - !ruby/object:Gem::Version
168
171
  version: '0'
169
172
  requirements: []
170
- rubygems_version: 3.6.9
173
+ rubygems_version: 4.0.3
171
174
  specification_version: 4
172
175
  summary: CLI tool for managing shared AI agent configurations
173
176
  test_files: []