riffer-rig 0.5.0 → 0.6.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 +22 -0
- data/README.md +43 -6
- data/lib/riffer/rig/cli.rb +46 -41
- data/lib/riffer/rig/credentials/resolution.rb +16 -0
- data/lib/riffer/rig/credentials.rb +151 -60
- data/lib/riffer/rig/env.rb +29 -0
- data/lib/riffer/rig/events/command_output.rb +27 -0
- data/lib/riffer/rig/events/event.rb +30 -0
- data/lib/riffer/rig/events/notify.rb +27 -0
- data/lib/riffer/rig/events/session_end.rb +24 -0
- data/lib/riffer/rig/events/session_start.rb +27 -0
- data/lib/riffer/rig/events/skill_activated.rb +24 -0
- data/lib/riffer/rig/events/turn_end.rb +32 -0
- data/lib/riffer/rig/events.rb +4 -0
- data/lib/riffer/rig/extension.rb +0 -4
- data/lib/riffer/rig/hosts/base.rb +36 -0
- data/lib/riffer/rig/hosts/mirror.rb +60 -0
- data/lib/riffer/rig/hosts/null.rb +36 -0
- data/lib/riffer/rig/provider_setup/field.rb +24 -0
- data/lib/riffer/rig/provider_setup.rb +84 -0
- data/lib/riffer/rig/registrar.rb +14 -2
- data/lib/riffer/rig/repl.rb +7 -8
- data/lib/riffer/rig/runtime/cancel_flag.rb +27 -0
- data/lib/riffer/rig/runtime.rb +104 -41
- data/lib/riffer/rig/settings/document.rb +6 -1
- data/lib/riffer/rig/settings/pricing.rb +0 -1
- data/lib/riffer/rig/settings.rb +47 -50
- data/lib/riffer/rig/token_tally.rb +0 -20
- data/lib/riffer/rig/tools/bash.rb +32 -11
- data/lib/riffer/rig/ui/animator.rb +2 -9
- data/lib/riffer/rig/ui/cursor.rb +0 -2
- data/lib/riffer/rig/ui/renderer.rb +9 -24
- data/lib/riffer/rig/ui/smoother.rb +11 -19
- data/lib/riffer/rig/ui/theme.rb +3 -5
- data/lib/riffer/rig/version.rb +1 -1
- data/lib/riffer/rig.rb +2 -7
- data/sig/generated/riffer/rig/cli.rbs +20 -12
- data/sig/generated/riffer/rig/credentials/resolution.rbs +13 -0
- data/sig/generated/riffer/rig/credentials.rbs +70 -41
- data/sig/generated/riffer/rig/env.rbs +20 -0
- data/sig/generated/riffer/rig/events/command_output.rbs +19 -0
- data/sig/generated/riffer/rig/events/event.rbs +20 -0
- data/sig/generated/riffer/rig/events/notify.rbs +19 -0
- data/sig/generated/riffer/rig/events/session_end.rbs +16 -0
- data/sig/generated/riffer/rig/events/session_start.rbs +19 -0
- data/sig/generated/riffer/rig/events/skill_activated.rbs +16 -0
- data/sig/generated/riffer/rig/events/turn_end.rbs +22 -0
- data/sig/generated/riffer/rig/events.rbs +4 -0
- data/sig/generated/riffer/rig/extension.rbs +0 -4
- data/sig/generated/riffer/rig/hosts/base.rbs +26 -0
- data/sig/generated/riffer/rig/hosts/mirror.rbs +41 -0
- data/sig/generated/riffer/rig/hosts/null.rbs +26 -0
- data/sig/generated/riffer/rig/provider_setup/field.rbs +22 -0
- data/sig/generated/riffer/rig/provider_setup.rbs +30 -0
- data/sig/generated/riffer/rig/registrar.rbs +10 -2
- data/sig/generated/riffer/rig/repl.rbs +0 -8
- data/sig/generated/riffer/rig/runtime/cancel_flag.rbs +19 -0
- data/sig/generated/riffer/rig/runtime.rbs +58 -39
- data/sig/generated/riffer/rig/settings/document.rbs +5 -1
- data/sig/generated/riffer/rig/settings/pricing.rbs +0 -1
- data/sig/generated/riffer/rig/settings.rbs +25 -46
- data/sig/generated/riffer/rig/token_tally.rbs +0 -19
- data/sig/generated/riffer/rig/tools/bash.rbs +13 -1
- data/sig/generated/riffer/rig/ui/animator.rbs +0 -8
- data/sig/generated/riffer/rig/ui/cursor.rbs +0 -2
- data/sig/generated/riffer/rig/ui/renderer.rbs +0 -24
- data/sig/generated/riffer/rig/ui/smoother.rbs +5 -18
- data/sig/generated/riffer/rig/ui/theme.rbs +2 -4
- data/sig/generated/riffer/rig.rbs +0 -7
- data/sig/manual/riffer/rig/cli.rbs +2 -4
- data/sig/manual/riffer/rig/credentials.rbs +2 -4
- data/sig/manual/riffer/rig/hosts.rbs +5 -0
- data/sig/manual/riffer/rig/settings.rbs +2 -4
- data/sig/manual/riffer/rig/ui/banner.rbs +2 -4
- metadata +38 -13
- data/lib/riffer/rig/host.rb +0 -52
- data/sig/generated/riffer/rig/host.rbs +0 -42
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b5a8d6de921c08c1ddb5caa12c5e22e0b3a68f7d26160b6fc732b647209b963
|
|
4
|
+
data.tar.gz: a37ce48ed7e46d9bca9a00db9a46411ecc0187550d58627879e9d97d36789650
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2b6f88978aa64314fb2b420047f97332090c3f8f699caa01564cd1ebf78dd5d862a22dca60e8e0734db647e2506aafbc5bd4de7a1ee1d800ee6a6edf1688b74
|
|
7
|
+
data.tar.gz: 3d6a59d2a2fa892430ddec4682c29fcc934d8568a3827ecf99546ea064b9ec246a1c5dbd5e96a0848328017408d17be91944bd2e84fd1eaf65dcc7707cf74389
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.0](https://github.com/bottrall/riffer-rig/compare/v0.5.0...v0.6.0) (2026-09-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* the flat `{"anthropic": "sk-..."}` auth.json shape is no longer read, with no migration. Stored keys must be re-entered or rewritten as `{"anthropic": {"type": "api_key", "api_key": "sk-..."}}`.
|
|
9
|
+
* Riffer::Rig::Host is removed. A host subclasses Riffer::Rig::Hosts::Base and implements all five methods; the null host is Riffer::Rig::Hosts::Null.
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add the rig event vocabulary and close ([#179](https://github.com/bottrall/riffer-rig/issues/179)) ([35bfa6e](https://github.com/bottrall/riffer-rig/commit/35bfa6e5dc1dc9825fd03843a4e69ec24e675b66))
|
|
14
|
+
* cancel a running turn from any thread ([#186](https://github.com/bottrall/riffer-rig/issues/186)) ([ec68188](https://github.com/bottrall/riffer-rig/commit/ec68188cc7dbcaf0cd56dd41d83cda216661fbe7))
|
|
15
|
+
* **deps:** upgrade riffer to 0.49.0 ([#188](https://github.com/bottrall/riffer-rig/issues/188)) ([821413f](https://github.com/bottrall/riffer-rig/commit/821413f5af65bfcb159ddad532deb3c15779112d))
|
|
16
|
+
* named prompt sections rendered every turn ([#184](https://github.com/bottrall/riffer-rig/issues/184)) ([d190a07](https://github.com/bottrall/riffer-rig/commit/d190a07dfa99764b82bba00745a07e7738c3532a))
|
|
17
|
+
* provider setup table and typed auth.json credentials ([#185](https://github.com/bottrall/riffer-rig/issues/185)) ([0a30c35](https://github.com/bottrall/riffer-rig/commit/0a30c357793716acfabcd1ddd41ea9f98c8a6a8f))
|
|
18
|
+
* store credentials on the Runtime ([#181](https://github.com/bottrall/riffer-rig/issues/181)) ([474a515](https://github.com/bottrall/riffer-rig/commit/474a51586011e413482f0275a7abd3a542e91914))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Code Refactoring
|
|
22
|
+
|
|
23
|
+
* make the host contract the abstract Hosts::Base ([#183](https://github.com/bottrall/riffer-rig/issues/183)) ([3fd4f14](https://github.com/bottrall/riffer-rig/commit/3fd4f14a193d546ae06e00776f209a52c0593854))
|
|
24
|
+
|
|
3
25
|
## [0.5.0](https://github.com/bottrall/riffer-rig/compare/v0.4.0...v0.5.0) (2026-09-19)
|
|
4
26
|
|
|
5
27
|
|
data/README.md
CHANGED
|
@@ -28,17 +28,54 @@ riffer
|
|
|
28
28
|
|
|
29
29
|
### Authentication
|
|
30
30
|
|
|
31
|
-
`riffer-rig` talks to Anthropic. Provide
|
|
31
|
+
`riffer-rig` talks to the provider named by the model's prefix — `anthropic/claude-sonnet-4-6`, the default, means Anthropic. Provide that provider's credentials in either of two ways:
|
|
32
32
|
|
|
33
|
-
- Set the
|
|
34
|
-
- Run `riffer` and paste
|
|
33
|
+
- Set the provider's environment variables, or
|
|
34
|
+
- Run `riffer` and paste each missing value when prompted on first launch. Secrets are saved to `~/.riffer/auth.json` (file permissions `600`); plain values such as an endpoint or region are saved to the `providers` block in `~/.riffer/settings.json`.
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Each value resolves on its own: environment variable first, then the stored value, then a provider-specific fallback (the AWS shared config for the Bedrock region), then the prompt. Optional values are never prompted for.
|
|
37
|
+
|
|
38
|
+
| Provider | Environment variables | Guide |
|
|
39
|
+
| ---------------- | ------------------------------------------------------------------------- | -------------------------------------------------- |
|
|
40
|
+
| `anthropic` | `ANTHROPIC_API_KEY` | [Anthropic](docs/PROVIDERS.md#anthropic) |
|
|
41
|
+
| `openai` | `OPENAI_API_KEY`; optional `OPENAI_BASE_URL` | [OpenAI](docs/PROVIDERS.md#openai) |
|
|
42
|
+
| `gemini` | `GEMINI_API_KEY` | [Gemini](docs/PROVIDERS.md#gemini) |
|
|
43
|
+
| `openrouter` | `OPENROUTER_API_KEY` | [OpenRouter](docs/PROVIDERS.md#openrouter) |
|
|
44
|
+
| `azure_openai` | `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_API_KEY` | [Azure OpenAI](docs/PROVIDERS.md#azure-openai) |
|
|
45
|
+
| `amazon_bedrock` | `AWS_REGION` or `AWS_DEFAULT_REGION`; optional `AWS_BEARER_TOKEN_BEDROCK` | [Amazon Bedrock](docs/PROVIDERS.md#amazon-bedrock) |
|
|
46
|
+
|
|
47
|
+
`auth.json` holds one typed entry per provider, and a stored secret may be a literal, a `$ENV_VAR` reference or a `!shell command` whose stdout is the secret:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"anthropic": { "type": "api_key", "api_key": "sk-ant-…" },
|
|
52
|
+
"openai": { "type": "api_key", "api_key": "!security find-generic-password -s openai -w" }
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The flat `{"anthropic": "sk-…"}` shape earlier versions wrote is no longer read; paste the key again when prompted. [Providers](docs/PROVIDERS.md) has the full format.
|
|
37
57
|
|
|
38
58
|
### Configuration
|
|
39
59
|
|
|
40
|
-
- `RIFFER_MODEL` — override the default model.
|
|
41
60
|
- `AGENTS.md` — if present, an `AGENTS.md` in the current working directory and/or a global `~/.riffer/AGENTS.md` is loaded as additional context.
|
|
61
|
+
- `~/.riffer/settings.json` — optional user settings. Every key is optional:
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"model": "anthropic/claude-sonnet-4-6",
|
|
66
|
+
"reasoning": "low",
|
|
67
|
+
"models": {
|
|
68
|
+
"anthropic/claude-sonnet-4-6": {
|
|
69
|
+
"input": 3.0,
|
|
70
|
+
"output": 15.0,
|
|
71
|
+
"cache_write": 3.75,
|
|
72
|
+
"cache_read": 0.3
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`models` holds pricing in USD per million tokens; a model without an entry shows no cost. `reasoning` is translated to the provider's own parameter — Anthropic accepts `low`, `medium`, `high`, `xhigh` and `max`; OpenAI and OpenRouter accept `low`, `medium`, `high` and `xhigh`. Omitting it, or supplying an unrecognised value, leaves the model's default reasoning behaviour unchanged.
|
|
42
79
|
|
|
43
80
|
## Development
|
|
44
81
|
|
|
@@ -62,7 +99,7 @@ PR titles are [conventional commits](https://www.conventionalcommits.org/) and a
|
|
|
62
99
|
|
|
63
100
|
[release-please](https://github.com/googleapis/release-please) keeps a release PR open that bumps `lib/riffer/rig/version.rb` and writes `CHANGELOG.md`. Merging that PR tags `vX.Y.Z`, creates the GitHub Release and publishes the gem to RubyGems.org through Trusted Publishing; nothing is pushed by hand.
|
|
64
101
|
|
|
65
|
-
riffer is pinned to one minor (`~> 0.
|
|
102
|
+
riffer is pinned to one minor (`~> 0.49.0`) because its 0.x minors may break. When Dependabot opens the riffer bump, retitle it `feat(deps):` or `fix(deps):` before merging so it releases and appears in the changelog.
|
|
66
103
|
|
|
67
104
|
## Contributing
|
|
68
105
|
|
data/lib/riffer/rig/cli.rb
CHANGED
|
@@ -5,26 +5,22 @@ require 'io/console'
|
|
|
5
5
|
module Riffer::Rig::CLI
|
|
6
6
|
extend self
|
|
7
7
|
|
|
8
|
-
PROVIDER_URLS = {
|
|
9
|
-
'anthropic' => 'https://console.anthropic.com/settings/keys',
|
|
10
|
-
'openai' => 'https://platform.openai.com/api-keys',
|
|
11
|
-
'gemini' => 'https://aistudio.google.com/app/apikey',
|
|
12
|
-
'openrouter' => 'https://openrouter.ai/keys'
|
|
13
|
-
}.freeze #: Hash[String, String]
|
|
14
|
-
|
|
15
8
|
# @rbs output: IO
|
|
16
9
|
# @rbs input: IO
|
|
10
|
+
# @rbs model: String
|
|
11
|
+
# @rbs env: Riffer::Rig::Env
|
|
17
12
|
# @rbs return: Integer
|
|
18
|
-
def start(output: $stdout, input: $stdin)
|
|
19
|
-
theme = Riffer::Rig::UI::Theme.for(output)
|
|
13
|
+
def start(output: $stdout, input: $stdin, model: Riffer::Rig::Settings.model, env: Riffer::Rig::Env.new)
|
|
14
|
+
theme = Riffer::Rig::UI::Theme.for(output, env:)
|
|
20
15
|
|
|
21
|
-
model = Riffer::Rig::Settings.model
|
|
22
16
|
provider = Riffer::Rig::Settings.provider_for(model)
|
|
23
17
|
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
if provider
|
|
19
|
+
values = credentials_for(provider, theme, output:, input:, env:)
|
|
20
|
+
return 1 if values.nil?
|
|
26
21
|
|
|
27
|
-
|
|
22
|
+
Riffer::Rig::Credentials.apply(provider, values)
|
|
23
|
+
end
|
|
28
24
|
|
|
29
25
|
agent = Riffer::Rig::CodingAgent.new
|
|
30
26
|
animator = Riffer::Rig::UI::Animator.new(io: output, theme:)
|
|
@@ -40,48 +36,57 @@ module Riffer::Rig::CLI
|
|
|
40
36
|
|
|
41
37
|
private
|
|
42
38
|
|
|
43
|
-
# @rbs provider: String
|
|
44
|
-
# @rbs
|
|
45
|
-
# @rbs
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
# @rbs provider: String
|
|
40
|
+
# @rbs theme: Riffer::Rig::UI::Theme
|
|
41
|
+
# @rbs output: IO
|
|
42
|
+
# @rbs input: IO
|
|
43
|
+
# @rbs env: Riffer::Rig::Env
|
|
44
|
+
# @rbs return: Hash[Symbol, String]?
|
|
45
|
+
def credentials_for(provider, theme, output:, input:, env:)
|
|
46
|
+
resolution = Riffer::Rig::Credentials.resolve(provider, host: Riffer::Rig::Hosts::Null.new, env:)
|
|
47
|
+
return resolution.values if resolution.missing.empty?
|
|
48
|
+
|
|
49
|
+
onboard(provider, resolution, theme, output:, input:)
|
|
53
50
|
end
|
|
54
51
|
|
|
55
|
-
# @rbs provider: String
|
|
52
|
+
# @rbs provider: String
|
|
53
|
+
# @rbs resolution: Riffer::Rig::Credentials::Resolution
|
|
56
54
|
# @rbs theme: Riffer::Rig::UI::Theme
|
|
57
55
|
# @rbs output: IO
|
|
58
56
|
# @rbs input: IO
|
|
59
|
-
# @rbs return: String?
|
|
60
|
-
def onboard(provider, theme, output:, input:)
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
# @rbs return: Hash[Symbol, String]?
|
|
58
|
+
def onboard(provider, resolution, theme, output:, input:)
|
|
59
|
+
setup = Riffer::Rig::ProviderSetup.for(provider)
|
|
60
|
+
missing = setup.fields.select { |field| resolution.missing.include?(field.name) }
|
|
63
61
|
|
|
64
62
|
output.puts(theme.cyan('♪ welcome to riffer-rig ♪'))
|
|
65
|
-
output.puts(theme.grey("No #{
|
|
66
|
-
output.print("#{theme.pink('›')} Paste your #{name} API key #{theme.grey('(hidden)')}: ")
|
|
67
|
-
|
|
68
|
-
key = read_secret(input).to_s.strip
|
|
69
|
-
output.puts
|
|
63
|
+
output.puts(theme.grey("No #{provider} credentials found. Create them at #{setup.url || 'your provider'}"))
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
answers = missing.to_h { |field| [field.name, prompt(provider, field, theme, output:, input:)] }
|
|
66
|
+
if answers.values.any?(&:empty?)
|
|
67
|
+
env_vars = missing.flat_map(&:env).join(', ')
|
|
68
|
+
output.puts(theme.grey("Nothing entered. Set #{env_vars} or re-run riffer to try again."))
|
|
73
69
|
return nil
|
|
74
70
|
end
|
|
75
71
|
|
|
76
|
-
Riffer::Rig::Credentials.
|
|
77
|
-
output.puts(theme.grey("Saved
|
|
78
|
-
|
|
72
|
+
Riffer::Rig::Credentials.store(provider, answers)
|
|
73
|
+
output.puts(theme.grey("Saved under #{File.dirname(Riffer::Rig::Credentials::PATH)} (auth.json permissions 600)."))
|
|
74
|
+
resolution.values.merge(answers)
|
|
79
75
|
end
|
|
80
76
|
|
|
81
|
-
# @rbs provider: String
|
|
77
|
+
# @rbs provider: String
|
|
78
|
+
# @rbs field: Riffer::Rig::ProviderSetup::Field
|
|
79
|
+
# @rbs theme: Riffer::Rig::UI::Theme
|
|
80
|
+
# @rbs output: IO
|
|
81
|
+
# @rbs input: IO
|
|
82
82
|
# @rbs return: String
|
|
83
|
-
def
|
|
84
|
-
|
|
83
|
+
def prompt(provider, field, theme, output:, input:)
|
|
84
|
+
hint = field.secret ? " #{theme.grey('(hidden)')}" : ''
|
|
85
|
+
output.print("#{theme.pink('›')} Paste your #{provider} #{field.name}#{hint}: ")
|
|
86
|
+
|
|
87
|
+
answer = (field.secret ? read_secret(input) : input.gets).to_s.strip
|
|
88
|
+
output.puts
|
|
89
|
+
answer
|
|
85
90
|
end
|
|
86
91
|
|
|
87
92
|
# @rbs input: IO
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Credentials::Resolution
|
|
4
|
+
# @dynamic values, missing
|
|
5
|
+
attr_reader :values #: Hash[Symbol, String]
|
|
6
|
+
attr_reader :missing #: Array[Symbol]
|
|
7
|
+
|
|
8
|
+
# @rbs values: Hash[Symbol, String]
|
|
9
|
+
# @rbs missing: Array[Symbol]
|
|
10
|
+
# @rbs return: void
|
|
11
|
+
def initialize(values:, missing:)
|
|
12
|
+
@values = values.freeze
|
|
13
|
+
@missing = missing.freeze
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -2,88 +2,179 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require 'open3'
|
|
5
6
|
|
|
6
|
-
# Reads and writes API keys for each supported provider in
|
|
7
|
-
# <tt>~/.riffer/auth.json</tt>.
|
|
8
|
-
#
|
|
9
|
-
# Keys are stored under the provider name, e.g.:
|
|
10
|
-
#
|
|
11
|
-
# {
|
|
12
|
-
# "anthropic": "sk-ant-...",
|
|
13
|
-
# "openai": "sk-...",
|
|
14
|
-
# "gemini": "...",
|
|
15
|
-
# "openrouter": "sk-or-..."
|
|
16
|
-
# }
|
|
17
|
-
#
|
|
18
|
-
# The environment variable checked per provider:
|
|
19
|
-
#
|
|
20
|
-
# anthropic → ANTHROPIC_API_KEY
|
|
21
|
-
# openai → OPENAI_API_KEY
|
|
22
|
-
# gemini → GEMINI_API_KEY
|
|
23
|
-
# openrouter → OPENROUTER_API_KEY
|
|
24
|
-
#
|
|
25
7
|
module Riffer::Rig::Credentials
|
|
26
8
|
extend self
|
|
27
9
|
|
|
28
10
|
PATH = File.expand_path('~/.riffer/auth.json') #: String
|
|
29
11
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
12
|
+
ENTRY_TYPE = 'api_key'
|
|
13
|
+
|
|
14
|
+
# @rbs identifier: String | Symbol
|
|
15
|
+
# @rbs host: Riffer::Rig::Hosts::Base
|
|
16
|
+
# @rbs setup: Riffer::Rig::ProviderSetup
|
|
17
|
+
# @rbs env: Riffer::Rig::Env
|
|
18
|
+
# @rbs auth_path: String
|
|
19
|
+
# @rbs settings_path: String
|
|
20
|
+
# @rbs return: Resolution
|
|
21
|
+
def resolve(
|
|
22
|
+
identifier,
|
|
23
|
+
host:,
|
|
24
|
+
setup: Riffer::Rig::ProviderSetup.for(identifier),
|
|
25
|
+
env: Riffer::Rig::Env.new,
|
|
26
|
+
auth_path: PATH,
|
|
27
|
+
settings_path: Riffer::Rig::Settings::PATH
|
|
28
|
+
)
|
|
29
|
+
secrets = stored_secrets(identifier, auth_path)
|
|
30
|
+
plain = Riffer::Rig::Settings.provider_fields(identifier.to_s, path: settings_path)
|
|
31
|
+
found = setup.fields.to_h do |field|
|
|
32
|
+
[field.name, env.value_for(field) || stored_value(field, secrets, plain, env) || field.fallback&.call]
|
|
33
|
+
end.compact
|
|
34
|
+
required = setup.fields.select(&:required)
|
|
35
|
+
answers = ask_for(required.reject { |field| found.key?(field.name) }, identifier, host)
|
|
36
|
+
store(identifier, answers, setup:, auth_path:, settings_path:)
|
|
37
|
+
|
|
38
|
+
values = found.merge(answers)
|
|
39
|
+
Resolution.new(values:, missing: required.map(&:name) - values.keys)
|
|
47
40
|
end
|
|
48
41
|
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# @rbs
|
|
52
|
-
# @rbs
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
# @rbs identifier: String | Symbol
|
|
43
|
+
# @rbs values: Hash[Symbol, String]
|
|
44
|
+
# @rbs config: Riffer::Config
|
|
45
|
+
# @rbs return: void
|
|
46
|
+
def apply(identifier, values, config: Riffer.config)
|
|
47
|
+
return unless Riffer::Rig::ProviderSetup[identifier]
|
|
48
|
+
|
|
49
|
+
provider_config = config.public_send(identifier)
|
|
50
|
+
values.each { |name, value| provider_config.public_send(:"#{name}=", value) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @rbs identifier: String | Symbol
|
|
54
|
+
# @rbs values: Hash[Symbol, String]
|
|
55
|
+
# @rbs setup: Riffer::Rig::ProviderSetup
|
|
56
|
+
# @rbs auth_path: String
|
|
57
|
+
# @rbs settings_path: String
|
|
58
|
+
# @rbs return: void
|
|
59
|
+
def store(
|
|
60
|
+
identifier,
|
|
61
|
+
values,
|
|
62
|
+
setup: Riffer::Rig::ProviderSetup.for(identifier),
|
|
63
|
+
auth_path: PATH,
|
|
64
|
+
settings_path: Riffer::Rig::Settings::PATH
|
|
65
|
+
)
|
|
66
|
+
secret_names = setup.fields.select(&:secret).map { |field| field.name.to_s }
|
|
67
|
+
fields = values.transform_keys(&:to_s)
|
|
68
|
+
secrets = fields.slice(*secret_names)
|
|
69
|
+
plain = fields.except(*secret_names)
|
|
70
|
+
|
|
71
|
+
unless secrets.empty?
|
|
72
|
+
entries = read_auth(auth_path).merge(identifier.to_s => secrets) do |_identifier, stored, given|
|
|
73
|
+
stored.merge(given)
|
|
74
|
+
end
|
|
75
|
+
write_auth(auth_path, entries)
|
|
60
76
|
end
|
|
61
|
-
|
|
77
|
+
Riffer::Rig::Settings.store_provider(identifier.to_s, plain, path: settings_path) unless plain.empty?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# @rbs identifier: String | Symbol
|
|
81
|
+
# @rbs auth_path: String
|
|
82
|
+
# @rbs settings_path: String
|
|
83
|
+
# @rbs return: void
|
|
84
|
+
def remove(identifier, auth_path: PATH, settings_path: Riffer::Rig::Settings::PATH)
|
|
85
|
+
entries = read_auth(auth_path)
|
|
86
|
+
write_auth(auth_path, entries.except(identifier.to_s)) if entries.key?(identifier.to_s)
|
|
87
|
+
Riffer::Rig::Settings.remove_provider(identifier.to_s, path: settings_path)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @rbs identifier: String | Symbol
|
|
91
|
+
# @rbs setup: Riffer::Rig::ProviderSetup
|
|
92
|
+
# @rbs env: Riffer::Rig::Env
|
|
93
|
+
# @rbs auth_path: String
|
|
94
|
+
# @rbs return: :env | :stored | :chain | :missing
|
|
95
|
+
def status(identifier, setup: Riffer::Rig::ProviderSetup.for(identifier), env: Riffer::Rig::Env.new, auth_path: PATH)
|
|
96
|
+
secret_fields = setup.fields.select(&:secret)
|
|
97
|
+
return :env if secret_fields.any? { |field| env.value_for(field) }
|
|
98
|
+
|
|
99
|
+
stored = stored_secrets(identifier, auth_path)
|
|
100
|
+
return :stored if secret_fields.any? { |field| stored.key?(field.name.to_s) }
|
|
101
|
+
|
|
102
|
+
setup.chain ? :chain : :missing
|
|
62
103
|
end
|
|
63
104
|
|
|
64
105
|
private
|
|
65
106
|
|
|
66
|
-
# @rbs
|
|
67
|
-
# @rbs
|
|
107
|
+
# @rbs fields: Array[Riffer::Rig::ProviderSetup::Field]
|
|
108
|
+
# @rbs identifier: String | Symbol
|
|
109
|
+
# @rbs host: Riffer::Rig::Hosts::Base
|
|
110
|
+
# @rbs return: Hash[Symbol, String]
|
|
111
|
+
def ask_for(fields, identifier, host)
|
|
112
|
+
fields.to_h do |field|
|
|
113
|
+
[field.name, presence(host.ask("#{identifier} #{field.name}", secret: field.secret))]
|
|
114
|
+
end.compact
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @rbs field: Riffer::Rig::ProviderSetup::Field
|
|
118
|
+
# @rbs secrets: Hash[String, String]
|
|
119
|
+
# @rbs plain: Hash[String, String]
|
|
120
|
+
# @rbs env: Riffer::Rig::Env
|
|
121
|
+
# @rbs return: String?
|
|
122
|
+
def stored_value(field, secrets, plain, env)
|
|
123
|
+
field.secret ? expand(secrets[field.name.to_s], env) : plain[field.name.to_s]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @rbs value: String?
|
|
127
|
+
# @rbs env: Riffer::Rig::Env
|
|
128
|
+
# @rbs return: String?
|
|
129
|
+
def expand(value, env)
|
|
130
|
+
return nil if value.nil?
|
|
131
|
+
return env[value.delete_prefix('$')] if value.start_with?('$')
|
|
132
|
+
return presence(run(value.delete_prefix('!'))) if value.start_with?('!')
|
|
133
|
+
|
|
134
|
+
value
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @rbs command: String
|
|
68
138
|
# @rbs return: String?
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
139
|
+
def run(command)
|
|
140
|
+
stdout, status = Open3.capture2(command)
|
|
141
|
+
stdout if status.success?
|
|
142
|
+
end
|
|
72
143
|
|
|
73
|
-
|
|
144
|
+
# @rbs value: String?
|
|
145
|
+
# @rbs return: String?
|
|
146
|
+
def presence(value)
|
|
147
|
+
stripped = value.to_s.strip
|
|
148
|
+
stripped unless stripped.empty?
|
|
74
149
|
end
|
|
75
150
|
|
|
76
|
-
#
|
|
77
|
-
# malformed. Non-string values can't be keys, so they're dropped here rather
|
|
78
|
-
# than re-checked by every reader.
|
|
79
|
-
#
|
|
151
|
+
# @rbs identifier: String | Symbol
|
|
80
152
|
# @rbs path: String
|
|
81
153
|
# @rbs return: Hash[String, String]
|
|
82
|
-
def
|
|
154
|
+
def stored_secrets(identifier, path)
|
|
155
|
+
read_auth(path)[identifier.to_s] || {}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# @rbs path: String
|
|
159
|
+
# @rbs return: Hash[String, Hash[String, String]]
|
|
160
|
+
def read_auth(path)
|
|
83
161
|
return {} unless File.file?(path)
|
|
84
162
|
|
|
85
|
-
JSON.parse(File.read(path))
|
|
163
|
+
JSON.parse(File.read(path))
|
|
164
|
+
.select { |_identifier, entry| entry.is_a?(Hash) && entry['type'] == ENTRY_TYPE }
|
|
165
|
+
.transform_values { |entry| entry.except('type').select { |_name, value| value.is_a?(String) } }
|
|
86
166
|
rescue JSON::ParserError
|
|
87
167
|
{}
|
|
88
168
|
end
|
|
169
|
+
|
|
170
|
+
# @rbs path: String
|
|
171
|
+
# @rbs entries: Hash[String, Hash[String, String]]
|
|
172
|
+
# @rbs return: void
|
|
173
|
+
def write_auth(path, entries)
|
|
174
|
+
FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
|
|
175
|
+
data = entries.transform_values { |fields| { 'type' => ENTRY_TYPE }.merge(fields) }
|
|
176
|
+
File.open(path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
|
|
177
|
+
file.write(JSON.pretty_generate(data))
|
|
178
|
+
end
|
|
179
|
+
end
|
|
89
180
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Env
|
|
4
|
+
# @rbs @source: Hash[String, String]
|
|
5
|
+
|
|
6
|
+
# @dynamic no_color
|
|
7
|
+
attr_reader :no_color #: bool
|
|
8
|
+
|
|
9
|
+
# @rbs source: Hash[String, String]
|
|
10
|
+
# @rbs return: void
|
|
11
|
+
def initialize(source = ENV.to_h)
|
|
12
|
+
@source = source.freeze
|
|
13
|
+
@no_color = @source.key?('NO_COLOR')
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @rbs name: String
|
|
18
|
+
# @rbs return: String?
|
|
19
|
+
def [](name)
|
|
20
|
+
stripped = @source[name].to_s.strip
|
|
21
|
+
stripped unless stripped.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @rbs field: Riffer::Rig::ProviderSetup::Field
|
|
25
|
+
# @rbs return: String?
|
|
26
|
+
def value_for(field)
|
|
27
|
+
field.env.filter_map { |name| self[name] }.first
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Events::CommandOutput < Riffer::Rig::Events::Event
|
|
4
|
+
# @dynamic command, text
|
|
5
|
+
attr_reader :command #: String
|
|
6
|
+
attr_reader :text #: String
|
|
7
|
+
|
|
8
|
+
# @rbs command: String
|
|
9
|
+
# @rbs text: String
|
|
10
|
+
# @rbs return: void
|
|
11
|
+
def initialize(command, text)
|
|
12
|
+
super()
|
|
13
|
+
@command = command
|
|
14
|
+
@text = text
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @rbs return: Symbol
|
|
19
|
+
def type
|
|
20
|
+
:command_output
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
24
|
+
def to_h
|
|
25
|
+
{ type: type, command: command, text: text }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Events::Event
|
|
4
|
+
# @rbs return: Symbol
|
|
5
|
+
def type
|
|
6
|
+
raise NotImplementedError, "#{self.class.name} must define type"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
10
|
+
def to_h
|
|
11
|
+
raise NotImplementedError, "#{self.class.name} must define to_h"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @rbs other: untyped
|
|
15
|
+
# @rbs return: bool
|
|
16
|
+
def ==(other)
|
|
17
|
+
other.class == self.class && other.to_h == to_h
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @rbs return: Integer
|
|
21
|
+
def hash
|
|
22
|
+
[self.class, to_h].hash
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @rbs other: untyped
|
|
26
|
+
# @rbs return: bool
|
|
27
|
+
def eql?(other)
|
|
28
|
+
self == other
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Events::Notify < Riffer::Rig::Events::Event
|
|
4
|
+
# @dynamic message, level
|
|
5
|
+
attr_reader :message #: String?
|
|
6
|
+
attr_reader :level #: Symbol
|
|
7
|
+
|
|
8
|
+
# @rbs message: String?
|
|
9
|
+
# @rbs level: Symbol
|
|
10
|
+
# @rbs return: void
|
|
11
|
+
def initialize(message, level)
|
|
12
|
+
super()
|
|
13
|
+
@message = message
|
|
14
|
+
@level = level
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @rbs return: Symbol
|
|
19
|
+
def type
|
|
20
|
+
:notify
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
24
|
+
def to_h
|
|
25
|
+
{ type: type, message: message, level: level }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Events::SessionEnd < Riffer::Rig::Events::Event
|
|
4
|
+
# @dynamic reason
|
|
5
|
+
attr_reader :reason #: Symbol
|
|
6
|
+
|
|
7
|
+
# @rbs reason: Symbol
|
|
8
|
+
# @rbs return: void
|
|
9
|
+
def initialize(reason)
|
|
10
|
+
super()
|
|
11
|
+
@reason = reason
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @rbs return: Symbol
|
|
16
|
+
def type
|
|
17
|
+
:session_end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
21
|
+
def to_h
|
|
22
|
+
{ type: type, reason: reason }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Riffer::Rig::Events::SessionStart < Riffer::Rig::Events::Event
|
|
4
|
+
# @dynamic id, reason
|
|
5
|
+
attr_reader :id #: String
|
|
6
|
+
attr_reader :reason #: Symbol
|
|
7
|
+
|
|
8
|
+
# @rbs id: String
|
|
9
|
+
# @rbs reason: Symbol
|
|
10
|
+
# @rbs return: void
|
|
11
|
+
def initialize(id, reason)
|
|
12
|
+
super()
|
|
13
|
+
@id = id
|
|
14
|
+
@reason = reason
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @rbs return: Symbol
|
|
19
|
+
def type
|
|
20
|
+
:session_start
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @rbs return: Hash[Symbol, untyped]
|
|
24
|
+
def to_h
|
|
25
|
+
{ type: type, id: id, reason: reason }
|
|
26
|
+
end
|
|
27
|
+
end
|