shai-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +192 -0
- data/bin/shai +23 -0
- data/lib/shai/api_client.rb +146 -0
- data/lib/shai/cli.rb +96 -0
- data/lib/shai/commands/auth.rb +70 -0
- data/lib/shai/commands/config.rb +168 -0
- data/lib/shai/commands/configurations.rb +393 -0
- data/lib/shai/commands/sync.rb +464 -0
- data/lib/shai/configuration.rb +65 -0
- data/lib/shai/credentials.rb +79 -0
- data/lib/shai/ui.rb +201 -0
- data/lib/shai/version.rb +5 -0
- data/lib/shai.rb +54 -0
- metadata +158 -0
data/lib/shai/ui.rb
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
require "tty-prompt"
|
|
5
|
+
require "tty-spinner"
|
|
6
|
+
require "tty-table"
|
|
7
|
+
|
|
8
|
+
module Shai
|
|
9
|
+
class UI
|
|
10
|
+
def initialize(color: true)
|
|
11
|
+
@pastel = Pastel.new(enabled: color)
|
|
12
|
+
@prompt = TTY::Prompt.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Output helpers
|
|
16
|
+
def success(message)
|
|
17
|
+
puts @pastel.green("✓ #{message}")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def error(message)
|
|
21
|
+
puts @pastel.red("Error: #{message}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def warning(message)
|
|
25
|
+
puts @pastel.yellow("Warning: #{message}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def info(message)
|
|
29
|
+
puts message
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def blank
|
|
33
|
+
puts
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def header(title)
|
|
37
|
+
puts @pastel.bold(title)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def indent(text, spaces: 2)
|
|
41
|
+
prefix = " " * spaces
|
|
42
|
+
puts text.lines.map { |line| "#{prefix}#{line}" }.join
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Formatting
|
|
46
|
+
def dim(text)
|
|
47
|
+
@pastel.dim(text)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def bold(text)
|
|
51
|
+
@pastel.bold(text)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def cyan(text)
|
|
55
|
+
@pastel.cyan(text)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def yellow(text)
|
|
59
|
+
@pastel.yellow(text)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def green(text)
|
|
63
|
+
@pastel.green(text)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def red(text)
|
|
67
|
+
@pastel.red(text)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Interactive prompts
|
|
71
|
+
def ask(question, **options)
|
|
72
|
+
@prompt.ask(question, **options)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def mask(question, **options)
|
|
76
|
+
@prompt.mask(question, **options)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def yes?(question, default: false)
|
|
80
|
+
require "io/console"
|
|
81
|
+
|
|
82
|
+
hint = default ? "(Y/n)" : "(y/N)"
|
|
83
|
+
print "#{question} #{hint} "
|
|
84
|
+
$stdout.flush
|
|
85
|
+
|
|
86
|
+
loop do
|
|
87
|
+
key = $stdin.getch
|
|
88
|
+
case key
|
|
89
|
+
when "y", "Y"
|
|
90
|
+
puts "y"
|
|
91
|
+
return true
|
|
92
|
+
when "n", "N"
|
|
93
|
+
puts "n"
|
|
94
|
+
return false
|
|
95
|
+
when "\r", "\n"
|
|
96
|
+
puts default ? "y" : "n"
|
|
97
|
+
return default
|
|
98
|
+
when "\u0003" # Ctrl+C
|
|
99
|
+
puts
|
|
100
|
+
raise Interrupt
|
|
101
|
+
end
|
|
102
|
+
# Ignore other keys, keep waiting
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def select(question, choices, **options)
|
|
107
|
+
@prompt.select(question, choices, **options)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Spinner for async operations
|
|
111
|
+
def spinner(message)
|
|
112
|
+
spinner = TTY::Spinner.new("[:spinner] #{message}", format: :dots)
|
|
113
|
+
spinner.auto_spin
|
|
114
|
+
result = yield
|
|
115
|
+
spinner.success
|
|
116
|
+
result
|
|
117
|
+
rescue => e
|
|
118
|
+
spinner.error
|
|
119
|
+
raise e
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Table output
|
|
123
|
+
def table(headers, rows)
|
|
124
|
+
table = TTY::Table.new(header: headers, rows: rows)
|
|
125
|
+
puts table.render(:unicode, padding: [0, 1])
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Diff output
|
|
129
|
+
def diff(text)
|
|
130
|
+
text.each_line do |line|
|
|
131
|
+
case line[0]
|
|
132
|
+
when "+"
|
|
133
|
+
puts @pastel.green(line)
|
|
134
|
+
when "-"
|
|
135
|
+
puts @pastel.red(line)
|
|
136
|
+
when "@"
|
|
137
|
+
puts @pastel.cyan(line)
|
|
138
|
+
else
|
|
139
|
+
puts line
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Configuration display
|
|
145
|
+
def display_configuration(config, detailed: false)
|
|
146
|
+
name = config["slug"] || config["name"]
|
|
147
|
+
# owner can be a string (username) or a hash with "username" key
|
|
148
|
+
owner = config["owner"]
|
|
149
|
+
owner = owner["username"] if owner.is_a?(Hash)
|
|
150
|
+
full_name = owner ? "#{owner}/#{name}" : name
|
|
151
|
+
visibility = config["visibility"]
|
|
152
|
+
stars = config["stars_count"] || 0
|
|
153
|
+
description = config["description"]
|
|
154
|
+
|
|
155
|
+
line = " #{full_name}"
|
|
156
|
+
line += " (#{visibility})" if visibility == "private"
|
|
157
|
+
line += " #{yellow("★")} #{stars}" if stars.positive?
|
|
158
|
+
puts line
|
|
159
|
+
|
|
160
|
+
if description && !description.empty?
|
|
161
|
+
puts " #{dim(description)}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if detailed
|
|
165
|
+
tags = config["tags"] || []
|
|
166
|
+
puts " #{dim("Tags: #{tags.join(", ")}")}" if tags.any?
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Tree display
|
|
171
|
+
def display_tree(items, prefix: "")
|
|
172
|
+
items.each_with_index do |item, index|
|
|
173
|
+
is_last = index == items.length - 1
|
|
174
|
+
connector = is_last ? "└── " : "├── "
|
|
175
|
+
puts "#{prefix}#{connector}#{item}"
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# File operation display
|
|
180
|
+
def display_file_operation(operation, path)
|
|
181
|
+
case operation
|
|
182
|
+
when :created
|
|
183
|
+
puts " #{green("Created")} #{path}"
|
|
184
|
+
when :modified
|
|
185
|
+
puts " #{yellow("Modified")} #{path}"
|
|
186
|
+
when :updated
|
|
187
|
+
puts " #{yellow("Updated")} #{path}"
|
|
188
|
+
when :deleted
|
|
189
|
+
puts " #{red("Deleted")} #{path}"
|
|
190
|
+
when :uploaded
|
|
191
|
+
puts " #{cyan("Uploading")} #{path}"
|
|
192
|
+
when :would_create
|
|
193
|
+
puts " #{path}"
|
|
194
|
+
when :would_update
|
|
195
|
+
puts " #{yellow("~")} #{path}"
|
|
196
|
+
when :conflict
|
|
197
|
+
puts " #{red(path)}"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
data/lib/shai/version.rb
ADDED
data/lib/shai.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "shai/version"
|
|
4
|
+
require_relative "shai/configuration"
|
|
5
|
+
require_relative "shai/credentials"
|
|
6
|
+
require_relative "shai/api_client"
|
|
7
|
+
require_relative "shai/cli"
|
|
8
|
+
|
|
9
|
+
module Shai
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
|
|
12
|
+
class AuthenticationError < Error; end
|
|
13
|
+
|
|
14
|
+
class NotFoundError < Error; end
|
|
15
|
+
|
|
16
|
+
class PermissionDeniedError < Error; end
|
|
17
|
+
|
|
18
|
+
class NetworkError < Error; end
|
|
19
|
+
|
|
20
|
+
class InvalidConfigurationError < Error; end
|
|
21
|
+
|
|
22
|
+
# Exit codes as specified in tech spec
|
|
23
|
+
EXIT_SUCCESS = 0
|
|
24
|
+
EXIT_GENERAL_ERROR = 1
|
|
25
|
+
EXIT_AUTH_REQUIRED = 2
|
|
26
|
+
EXIT_PERMISSION_DENIED = 3
|
|
27
|
+
EXIT_NOT_FOUND = 4
|
|
28
|
+
EXIT_NETWORK_ERROR = 5
|
|
29
|
+
EXIT_INVALID_INPUT = 6
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def configuration
|
|
33
|
+
@configuration ||= Configuration.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def configure
|
|
37
|
+
yield(configuration)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def credentials
|
|
41
|
+
@credentials ||= Credentials.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def api_client
|
|
45
|
+
@api_client ||= ApiClient.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def reset!
|
|
49
|
+
@configuration = nil
|
|
50
|
+
@credentials = nil
|
|
51
|
+
@api_client = nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: shai-cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Sebastian Jimenez
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: thor
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.9'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.9'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: tty-prompt
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.23'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.23'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: tty-spinner
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.9'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.9'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: tty-table
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0.12'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0.12'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: pastel
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.8'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.8'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: diffy
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '3.4'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '3.4'
|
|
110
|
+
description: A command-line interface for shaicli.dev - download, share, and sync
|
|
111
|
+
AI agent configurations (Claude, Cursor, etc.) across projects and teams.
|
|
112
|
+
email:
|
|
113
|
+
- sebastian@infinitlab.co
|
|
114
|
+
executables:
|
|
115
|
+
- shai
|
|
116
|
+
extensions: []
|
|
117
|
+
extra_rdoc_files: []
|
|
118
|
+
files:
|
|
119
|
+
- LICENSE
|
|
120
|
+
- README.md
|
|
121
|
+
- bin/shai
|
|
122
|
+
- lib/shai.rb
|
|
123
|
+
- lib/shai/api_client.rb
|
|
124
|
+
- lib/shai/cli.rb
|
|
125
|
+
- lib/shai/commands/auth.rb
|
|
126
|
+
- lib/shai/commands/config.rb
|
|
127
|
+
- lib/shai/commands/configurations.rb
|
|
128
|
+
- lib/shai/commands/sync.rb
|
|
129
|
+
- lib/shai/configuration.rb
|
|
130
|
+
- lib/shai/credentials.rb
|
|
131
|
+
- lib/shai/ui.rb
|
|
132
|
+
- lib/shai/version.rb
|
|
133
|
+
homepage: https://shaicli.dev
|
|
134
|
+
licenses:
|
|
135
|
+
- MIT
|
|
136
|
+
metadata:
|
|
137
|
+
homepage_uri: https://shaicli.dev
|
|
138
|
+
source_code_uri: https://github.com/infinitlab/shai-cli
|
|
139
|
+
changelog_uri: https://github.com/infinitlab/shai-cli/blob/main/CHANGELOG.md
|
|
140
|
+
rubygems_mfa_required: 'true'
|
|
141
|
+
rdoc_options: []
|
|
142
|
+
require_paths:
|
|
143
|
+
- lib
|
|
144
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
145
|
+
requirements:
|
|
146
|
+
- - ">="
|
|
147
|
+
- !ruby/object:Gem::Version
|
|
148
|
+
version: 3.1.0
|
|
149
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
150
|
+
requirements:
|
|
151
|
+
- - ">="
|
|
152
|
+
- !ruby/object:Gem::Version
|
|
153
|
+
version: '0'
|
|
154
|
+
requirements: []
|
|
155
|
+
rubygems_version: 3.6.9
|
|
156
|
+
specification_version: 4
|
|
157
|
+
summary: CLI tool for managing shared AI agent configurations
|
|
158
|
+
test_files: []
|