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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f13b2f27c4542d4f5fe83f06e781589bd69bde8bd618e7b8ce494fd1d4ab621b
|
|
4
|
+
data.tar.gz: 94129b59f660fcff737602a475b891d87526968bd3ee4b28d495012a225d6cd5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4d89e81bdb2d09f584d53fb9c652e61d38ec7e40378e2ccca88b12b75bed765dd0aa18692fd0599a17095ec331f0e5aabfdcabea1da5c89df9ad927cacb396dc
|
|
7
|
+
data.tar.gz: e636e3d6f766962c11df07de32e70fd62944e2e09c4a0e2acbb723291b75e5425e2b922a98e55e110bb12c3fd277d27993c9e9eda912713065f6897086ed5248
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 InfinitLab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# shai-cli
|
|
2
|
+
|
|
3
|
+
Command-line tool for managing and sharing AI agent configurations via [shaicli.dev](https://shaicli.dev).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### Quick install (recommended)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
curl -fsSL https://shaicli.dev/install.sh | bash
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Via RubyGems
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install shai-cli
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### From source
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/infinitlab/shai-cli.git
|
|
23
|
+
cd shai-cli
|
|
24
|
+
bundle install
|
|
25
|
+
bundle exec rake install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# Login to your shaicli.dev account
|
|
32
|
+
shai login
|
|
33
|
+
|
|
34
|
+
# Search for public configurations
|
|
35
|
+
shai search "claude code"
|
|
36
|
+
|
|
37
|
+
# Install a configuration to your project
|
|
38
|
+
shai install anthropic/claude-expert
|
|
39
|
+
|
|
40
|
+
# Create and share your own configuration
|
|
41
|
+
shai init
|
|
42
|
+
shai push
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Commands
|
|
46
|
+
|
|
47
|
+
### Authentication
|
|
48
|
+
|
|
49
|
+
| Command | Description |
|
|
50
|
+
|---------|-------------|
|
|
51
|
+
| `shai login` | Log in to shaicli.dev |
|
|
52
|
+
| `shai logout` | Log out and remove stored credentials |
|
|
53
|
+
| `shai whoami` | Show current authentication status |
|
|
54
|
+
|
|
55
|
+
### Discovery
|
|
56
|
+
|
|
57
|
+
| Command | Description |
|
|
58
|
+
|---------|-------------|
|
|
59
|
+
| `shai list` | List your configurations |
|
|
60
|
+
| `shai search <query>` | Search public configurations |
|
|
61
|
+
|
|
62
|
+
### Using Configurations
|
|
63
|
+
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---------|-------------|
|
|
66
|
+
| `shai install <config>` | Install a configuration to local project |
|
|
67
|
+
| `shai uninstall <config>` | Remove an installed configuration |
|
|
68
|
+
|
|
69
|
+
### Authoring Configurations
|
|
70
|
+
|
|
71
|
+
| Command | Description |
|
|
72
|
+
|---------|-------------|
|
|
73
|
+
| `shai init` | Initialize a new configuration |
|
|
74
|
+
| `shai push` | Push local changes to remote |
|
|
75
|
+
| `shai status` | Show local changes vs remote |
|
|
76
|
+
| `shai diff` | Show diff between local and remote |
|
|
77
|
+
| `shai config show` | Show configuration details |
|
|
78
|
+
| `shai config set <key> <value>` | Update configuration metadata |
|
|
79
|
+
| `shai delete <slug>` | Delete a configuration from remote |
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
### Environment Variables
|
|
84
|
+
|
|
85
|
+
| Variable | Description | Default |
|
|
86
|
+
|----------|-------------|---------|
|
|
87
|
+
| `SHAI_API_URL` | API endpoint URL | `https://shaicli.dev` |
|
|
88
|
+
| `SHAI_CONFIG_DIR` | Directory for credentials | `~/.config/shai` |
|
|
89
|
+
| `SHAI_TOKEN` | Override authentication token | - |
|
|
90
|
+
| `NO_COLOR` | Disable colored output | - |
|
|
91
|
+
|
|
92
|
+
### .shairc File
|
|
93
|
+
|
|
94
|
+
When authoring configurations, a `.shairc` file is created in your project root:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
# .shairc - Shai configuration
|
|
98
|
+
slug: my-config
|
|
99
|
+
include:
|
|
100
|
+
- .claude/**
|
|
101
|
+
- .cursor/**
|
|
102
|
+
exclude:
|
|
103
|
+
- "**/*.local.*"
|
|
104
|
+
- "**/.env"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| Field | Description |
|
|
108
|
+
|-------|-------------|
|
|
109
|
+
| `slug` | Unique identifier for your configuration |
|
|
110
|
+
| `include` | Glob patterns for files to include |
|
|
111
|
+
| `exclude` | Glob patterns for files to exclude |
|
|
112
|
+
|
|
113
|
+
## Examples
|
|
114
|
+
|
|
115
|
+
### Search by tags
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
shai search --tag claude --tag coding
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Install to specific directory
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
shai install anthropic/claude-expert --path ./my-project
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Preview installation without making changes
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
shai install anthropic/claude-expert --dry-run
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Force overwrite existing files
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
shai install anthropic/claude-expert --force
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Create a public configuration
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
shai init
|
|
143
|
+
# Follow prompts, select "public" visibility
|
|
144
|
+
shai push
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Update configuration metadata
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
shai config set name "My Updated Config"
|
|
151
|
+
shai config set visibility public
|
|
152
|
+
shai config set description "A better description"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Development
|
|
156
|
+
|
|
157
|
+
### Setup
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
bundle install
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Run tests
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
bundle exec rspec
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Run linter
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
bundle exec standardrb
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Local development
|
|
176
|
+
|
|
177
|
+
Create `.env.development`:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
SHAI_API_URL=http://localhost:3001
|
|
181
|
+
SHAI_CONFIG_DIR=.config/shai-dev
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Run commands in development mode:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
SHAI_ENV=development bundle exec bin/shai <command>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT
|
data/bin/shai
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Load environment variables from .env files (development only)
|
|
5
|
+
begin
|
|
6
|
+
require "dotenv"
|
|
7
|
+
|
|
8
|
+
env = ENV.fetch("SHAI_ENV", "development")
|
|
9
|
+
env_files = [
|
|
10
|
+
".env.#{env}.local",
|
|
11
|
+
".env.#{env}",
|
|
12
|
+
".env.local",
|
|
13
|
+
".env"
|
|
14
|
+
].select { |f| File.exist?(f) }
|
|
15
|
+
|
|
16
|
+
Dotenv.load(*env_files) if env_files.any?
|
|
17
|
+
rescue LoadError
|
|
18
|
+
# dotenv not available (production gem install), skip
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
require_relative "../lib/shai"
|
|
22
|
+
|
|
23
|
+
Shai::CLI.start(ARGV)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module Shai
|
|
8
|
+
class ApiClient
|
|
9
|
+
def initialize
|
|
10
|
+
@connection = build_connection
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Authentication
|
|
14
|
+
def login(identifier:, password:, client_name: nil)
|
|
15
|
+
client_name ||= default_client_name
|
|
16
|
+
post("/api/v1/cli/session", {
|
|
17
|
+
identifier: identifier,
|
|
18
|
+
password: password,
|
|
19
|
+
client_name: client_name
|
|
20
|
+
})
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Configurations
|
|
24
|
+
def list_configurations
|
|
25
|
+
get("/api/v1/configurations")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def search_configurations(query: nil, tags: [])
|
|
29
|
+
params = {}
|
|
30
|
+
params[:q] = query if query
|
|
31
|
+
params["tags[]"] = tags if tags.any?
|
|
32
|
+
get("/api/v1/configurations/search", params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def get_configuration(identifier)
|
|
36
|
+
get("/api/v1/configurations/#{encode_identifier(identifier)}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create_configuration(name:, description: nil, visibility: "private")
|
|
40
|
+
post("/api/v1/configurations", {
|
|
41
|
+
configuration: {
|
|
42
|
+
name: name,
|
|
43
|
+
description: description,
|
|
44
|
+
visibility: visibility
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def update_configuration(identifier, **attributes)
|
|
50
|
+
put("/api/v1/configurations/#{encode_identifier(identifier)}", {
|
|
51
|
+
configuration: attributes
|
|
52
|
+
})
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def delete_configuration(identifier)
|
|
56
|
+
delete("/api/v1/configurations/#{encode_identifier(identifier)}")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def get_tree(identifier)
|
|
60
|
+
get("/api/v1/configurations/#{encode_identifier(identifier)}/tree")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def update_tree(identifier, tree)
|
|
64
|
+
put("/api/v1/configurations/#{encode_identifier(identifier)}/tree", {tree: tree})
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Encode identifier for URL (handles owner/slug format)
|
|
68
|
+
def encode_identifier(identifier)
|
|
69
|
+
URI.encode_www_form_component(identifier)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def build_connection
|
|
75
|
+
Faraday.new(url: Shai.configuration.api_url) do |conn|
|
|
76
|
+
conn.request :json
|
|
77
|
+
conn.response :json, content_type: /\bjson$/
|
|
78
|
+
conn.adapter Faraday.default_adapter
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def get(path, params = {})
|
|
83
|
+
response = @connection.get(path, params) do |req|
|
|
84
|
+
add_auth_header(req)
|
|
85
|
+
end
|
|
86
|
+
handle_response(response)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def post(path, body)
|
|
90
|
+
response = @connection.post(path) do |req|
|
|
91
|
+
add_auth_header(req)
|
|
92
|
+
req.body = body
|
|
93
|
+
end
|
|
94
|
+
handle_response(response)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def put(path, body)
|
|
98
|
+
response = @connection.put(path) do |req|
|
|
99
|
+
add_auth_header(req)
|
|
100
|
+
req.body = body
|
|
101
|
+
end
|
|
102
|
+
handle_response(response)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def delete(path)
|
|
106
|
+
response = @connection.delete(path) do |req|
|
|
107
|
+
add_auth_header(req)
|
|
108
|
+
end
|
|
109
|
+
handle_response(response)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def add_auth_header(request)
|
|
113
|
+
token = Shai.configuration.token || Shai.credentials.token
|
|
114
|
+
request.headers["Authorization"] = "Bearer #{token}" if token
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def handle_response(response)
|
|
118
|
+
case response.status
|
|
119
|
+
when 200..299
|
|
120
|
+
response.body
|
|
121
|
+
when 401
|
|
122
|
+
raise AuthenticationError, response.body&.dig("error") || "Authentication failed"
|
|
123
|
+
when 403
|
|
124
|
+
raise PermissionDeniedError, response.body&.dig("error") || "Permission denied"
|
|
125
|
+
when 404
|
|
126
|
+
raise NotFoundError, response.body&.dig("error") || "Not found"
|
|
127
|
+
when 422
|
|
128
|
+
raise InvalidConfigurationError, response.body&.dig("error") || "Invalid request"
|
|
129
|
+
else
|
|
130
|
+
raise Error, response.body&.dig("error") || "Request failed with status #{response.status}"
|
|
131
|
+
end
|
|
132
|
+
rescue Faraday::ConnectionFailed, Faraday::TimeoutError
|
|
133
|
+
raise NetworkError, "Could not connect to #{Shai.configuration.api_url}. Check your internet connection."
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def default_client_name
|
|
137
|
+
hostname = begin
|
|
138
|
+
require "socket"
|
|
139
|
+
Socket.gethostname
|
|
140
|
+
rescue
|
|
141
|
+
"Unknown"
|
|
142
|
+
end
|
|
143
|
+
"CLI (#{hostname})"
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
data/lib/shai/cli.rb
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require_relative "commands/auth"
|
|
5
|
+
require_relative "commands/configurations"
|
|
6
|
+
require_relative "commands/sync"
|
|
7
|
+
require_relative "commands/config"
|
|
8
|
+
require_relative "ui"
|
|
9
|
+
|
|
10
|
+
module Shai
|
|
11
|
+
class CLI < Thor
|
|
12
|
+
include Commands::Auth
|
|
13
|
+
include Commands::Configurations
|
|
14
|
+
include Commands::Sync
|
|
15
|
+
include Commands::Config
|
|
16
|
+
|
|
17
|
+
def self.exit_on_failure?
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Custom help to group commands by category
|
|
22
|
+
def self.help(shell, subcommand = false)
|
|
23
|
+
shell.say "shai - Manage AI agent configurations"
|
|
24
|
+
shell.say ""
|
|
25
|
+
shell.say "USAGE:"
|
|
26
|
+
shell.say " shai <command> [options]"
|
|
27
|
+
shell.say ""
|
|
28
|
+
shell.say "AUTHENTICATION:"
|
|
29
|
+
shell.say " login Log in to shaicli.dev"
|
|
30
|
+
shell.say " logout Log out and remove stored credentials"
|
|
31
|
+
shell.say " whoami Show current authentication status"
|
|
32
|
+
shell.say ""
|
|
33
|
+
shell.say "DISCOVERY:"
|
|
34
|
+
shell.say " list List your configurations"
|
|
35
|
+
shell.say " search <query> Search public configurations"
|
|
36
|
+
shell.say ""
|
|
37
|
+
shell.say "USING CONFIGURATIONS (install to current project):"
|
|
38
|
+
shell.say " install <config> Install a configuration to local project"
|
|
39
|
+
shell.say " uninstall <config> Remove an installed configuration"
|
|
40
|
+
shell.say ""
|
|
41
|
+
shell.say "AUTHORING CONFIGURATIONS (create and publish):"
|
|
42
|
+
shell.say " init Initialize a new configuration"
|
|
43
|
+
shell.say " push Push local changes to remote"
|
|
44
|
+
shell.say " status Show local changes vs remote"
|
|
45
|
+
shell.say " diff Show diff between local and remote"
|
|
46
|
+
shell.say " config show Show configuration details"
|
|
47
|
+
shell.say " config set <k> <v> Update configuration metadata"
|
|
48
|
+
shell.say " delete <slug> Delete a configuration from remote"
|
|
49
|
+
shell.say ""
|
|
50
|
+
shell.say "OPTIONS:"
|
|
51
|
+
shell.say " -h, --help Show help for a command"
|
|
52
|
+
shell.say " -v, --version Show version"
|
|
53
|
+
shell.say " --verbose Enable verbose output"
|
|
54
|
+
shell.say " --no-color Disable colored output"
|
|
55
|
+
shell.say ""
|
|
56
|
+
shell.say "EXAMPLES:"
|
|
57
|
+
shell.say " shai login"
|
|
58
|
+
shell.say " shai search \"claude code\""
|
|
59
|
+
shell.say " shai install anthropic/claude-expert"
|
|
60
|
+
shell.say " shai init"
|
|
61
|
+
shell.say " shai push"
|
|
62
|
+
shell.say ""
|
|
63
|
+
shell.say "Run 'shai help <command>' for more information on a command."
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class_option :verbose, type: :boolean, default: false, desc: "Enable verbose output"
|
|
67
|
+
class_option :no_color, type: :boolean, default: false, desc: "Disable colored output"
|
|
68
|
+
|
|
69
|
+
desc "version", "Show version"
|
|
70
|
+
map %w[-v --version] => :version
|
|
71
|
+
def version
|
|
72
|
+
puts "shai #{Shai::VERSION}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def ui
|
|
78
|
+
@ui ||= UI.new(color: !options[:no_color])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def api
|
|
82
|
+
Shai.api_client
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def credentials
|
|
86
|
+
Shai.credentials
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def require_auth!
|
|
90
|
+
return if credentials.authenticated?
|
|
91
|
+
|
|
92
|
+
ui.error("Not logged in. Run `shai login` to authenticate.")
|
|
93
|
+
exit EXIT_AUTH_REQUIRED
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Shai
|
|
4
|
+
module Commands
|
|
5
|
+
module Auth
|
|
6
|
+
def self.included(base)
|
|
7
|
+
base.class_eval do
|
|
8
|
+
desc "login", "Log in to shaicli.dev"
|
|
9
|
+
def login
|
|
10
|
+
identifier = ui.ask("Email or username:")
|
|
11
|
+
password = ui.mask("Password:")
|
|
12
|
+
|
|
13
|
+
ui.blank
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
response = ui.spinner("Logging in...") do
|
|
17
|
+
api.login(identifier: identifier, password: password)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
data = response["data"]
|
|
21
|
+
credentials.save(
|
|
22
|
+
token: data["token"],
|
|
23
|
+
expires_at: data["expires_at"],
|
|
24
|
+
user: data["user"]
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
ui.success("Logged in as #{data.dig("user", "username")}")
|
|
28
|
+
ui.indent("Token expires: #{format_date(data["expires_at"])}")
|
|
29
|
+
ui.indent("Token stored in #{Shai.configuration.credentials_path}")
|
|
30
|
+
rescue AuthenticationError
|
|
31
|
+
ui.error("Invalid credentials")
|
|
32
|
+
exit EXIT_AUTH_REQUIRED
|
|
33
|
+
rescue NetworkError => e
|
|
34
|
+
ui.error(e.message)
|
|
35
|
+
exit EXIT_NETWORK_ERROR
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
desc "logout", "Log out and remove stored credentials"
|
|
40
|
+
def logout
|
|
41
|
+
credentials.clear
|
|
42
|
+
ui.success("Logged out successfully")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "whoami", "Show current authentication status"
|
|
46
|
+
def whoami
|
|
47
|
+
if credentials.authenticated?
|
|
48
|
+
name = credentials.display_name || credentials.username
|
|
49
|
+
ui.info("Logged in as #{credentials.username} (#{name})")
|
|
50
|
+
ui.info("Token expires: #{format_date(credentials.expires_at)}")
|
|
51
|
+
else
|
|
52
|
+
ui.info("Not logged in. Run `shai login` to authenticate.")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def format_date(date_string)
|
|
61
|
+
return "unknown" unless date_string
|
|
62
|
+
|
|
63
|
+
date = Time.parse(date_string)
|
|
64
|
+
date.strftime("%B %-d, %Y")
|
|
65
|
+
rescue ArgumentError
|
|
66
|
+
date_string
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|