pgai 0.1.1
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/MIT-LICENSE +20 -0
- data/README.md +41 -0
- data/bin/pgai +8 -0
- data/lib/pgai/cli/base.rb +21 -0
- data/lib/pgai/cli/env.rb +22 -0
- data/lib/pgai/cli/main.rb +31 -0
- data/lib/pgai/clone_manager.rb +143 -0
- data/lib/pgai/config.rb +109 -0
- data/lib/pgai/version.rb +3 -0
- data/lib/pgai.rb +14 -0
- metadata +116 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c342a96f17431de20953406ba211771e0b7647f6ec7e59377e50399d89ca63bc
|
4
|
+
data.tar.gz: e1737c3f34f8b2b832c9706019eec6169fb723eb27ffc260acd285a8e24fce1e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e1981687bdf6204b16e651f6313c3db1eb8e9691817b23f49c4f81611163a9ac3df7588ffe2b58dbae5c4adcd75e924ea6e2c4835fa24c29bc73421d7a07c027
|
7
|
+
data.tar.gz: 7a9a3fe849f9d5cb1d010fba31900f07ff0f10413f222a329a080d3517b44aa6b870f09e7d9995bbff573cf434ba09f4364126df3e8831df5938a69932fbb4e2
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2023 Marius Bobin
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# Pgai
|
2
|
+
|
3
|
+
Simple CLI wrapper around Postgres.ai CLI to drop directly into a PSQL session.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Install the gem by executing:
|
8
|
+
|
9
|
+
```shell
|
10
|
+
gem install pgai
|
11
|
+
```
|
12
|
+
|
13
|
+
## Configuration
|
14
|
+
|
15
|
+
For an overview of available commands, execute:
|
16
|
+
|
17
|
+
```shell
|
18
|
+
pgai -h
|
19
|
+
```
|
20
|
+
|
21
|
+
Before usage `pgai config` must be executed and at least an environment must be configured with `pgai env add`
|
22
|
+
|
23
|
+
Postgres.ai does not provide `dblab` binaries for `ARM` processors(yet?), to work around this:
|
24
|
+
|
25
|
+
- clone `https://gitlab.com/postgres-ai/database-lab`
|
26
|
+
- change [`GOARCH = amd64`](https://gitlab.com/postgres-ai/database-lab/-/blob/0ad2cdd8f1d851e4beda92e0577bb242c6a8dd7f/engine/Makefile#L6) to `GOARCH = arm64`
|
27
|
+
- execute [`make build`](https://gitlab.com/postgres-ai/database-lab/-/blob/0ad2cdd8f1d851e4beda92e0577bb242c6a8dd7f/engine/Makefile#L44-L47), we're interested only about building `CLI_BINARY`, the others could be removed.
|
28
|
+
- the `dblab` binary should be available in the `bin` directory
|
29
|
+
- run `pgai config --exe /path/to/bin/dblab` to set the path if it's not in `$PATH`.
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
```shell
|
34
|
+
pgai connect <env alias>
|
35
|
+
```
|
36
|
+
|
37
|
+
Multiple `connect` commands for an environment will connect to the same clone, it won't start a new one.
|
38
|
+
|
39
|
+
## Contributing
|
40
|
+
|
41
|
+
Bug reports and pull requests are welcome on GitLab at https://gitlab.com/mbobin/pgai.
|
data/bin/pgai
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "thor"
|
2
|
+
|
3
|
+
module Pgai::Cli
|
4
|
+
class Base < Thor
|
5
|
+
def self.exit_on_failure?
|
6
|
+
true
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def configuration
|
12
|
+
@configuration ||= load_and_validate_config!
|
13
|
+
end
|
14
|
+
|
15
|
+
def load_and_validate_config!
|
16
|
+
Pgai::Config.load.tap do |config|
|
17
|
+
raise "Access token is not set" unless config.access_token
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/pgai/cli/env.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Pgai::Cli
|
2
|
+
class Env < Base
|
3
|
+
desc "add", "Add new environment"
|
4
|
+
method_option :alias, aliases: "-a", desc: "This will be used internally for the connect command", required: true
|
5
|
+
method_option :id, aliases: "-i", desc: "Environment id from postgres.ai", required: true
|
6
|
+
method_option :port, aliases: "-p", desc: "Environment port from postgres.ai", required: true
|
7
|
+
def add
|
8
|
+
configuration.add_env(**options)
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "remove", "Remove the specified environment from the config"
|
12
|
+
method_option :alias, aliases: "-a", desc: "This will be used internally for the connect command", required: true
|
13
|
+
def remove
|
14
|
+
configuration.remove_env(options[:alias])
|
15
|
+
end
|
16
|
+
|
17
|
+
desc "list", "List all configured environments"
|
18
|
+
def list
|
19
|
+
puts JSON.pretty_generate(configuration.enviroments)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Pgai::Cli
|
2
|
+
class Main < Base
|
3
|
+
desc "config", "Configure CLI options"
|
4
|
+
method_option :prefix, aliases: "-p", desc: "Specify prefix to be used for clones", required: true
|
5
|
+
method_option :dbname, aliases: "-n", desc: "Specify database name to connect to", required: true
|
6
|
+
method_option :proxy, aliases: "-x", desc: "Specify proxy host name", required: true
|
7
|
+
method_option :exe, aliases: "-e", desc: "dblab binary executable path", required: true
|
8
|
+
def config
|
9
|
+
token = ask("Access token:", echo: false)
|
10
|
+
|
11
|
+
Pgai::Config.persist(options.merge(token: token))
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "connect database-name", "Create and connect to a thin clone database"
|
15
|
+
def connect(name)
|
16
|
+
env = configuration.enviroments.fetch(name)
|
17
|
+
|
18
|
+
Pgai::CloneManager.new(env, config: configuration).connect
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "destroy database-name", "Remove the thin clone and all port forwards"
|
22
|
+
def destroy(name)
|
23
|
+
env = configuration.enviroments.fetch(name)
|
24
|
+
|
25
|
+
Pgai::CloneManager.new(env, config: configuration).cleanup
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "env", "Manage environments"
|
29
|
+
subcommand "env", Pgai::Cli::Env
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "shellwords"
|
4
|
+
require "socket"
|
5
|
+
require "json"
|
6
|
+
require "securerandom"
|
7
|
+
|
8
|
+
module Pgai
|
9
|
+
class CloneManager
|
10
|
+
HOSTNAME = "127.0.0.1"
|
11
|
+
|
12
|
+
def initialize(environment, config:)
|
13
|
+
@environment = environment
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
def connect
|
18
|
+
configure_enviroment
|
19
|
+
|
20
|
+
psql find_or_create_clone
|
21
|
+
end
|
22
|
+
|
23
|
+
def cleanup
|
24
|
+
configure_enviroment
|
25
|
+
return unless find_raw_clone
|
26
|
+
|
27
|
+
dblab("clone", "destroy", clone_id, raw: true)
|
28
|
+
config.remove_clone(clone_id)
|
29
|
+
stop_port_forwards
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :environment, :config
|
35
|
+
|
36
|
+
def configure_enviroment
|
37
|
+
start_port_forward(enviroment_port)
|
38
|
+
|
39
|
+
dblab("init", "--url", "http://#{HOSTNAME}:#{enviroment_port}", "--token", config.access_token, "--environment-id", environment_id)
|
40
|
+
dblab("config", "switch", environment_id, raw: true)
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_or_create_clone
|
44
|
+
find_clone || create_clone
|
45
|
+
end
|
46
|
+
|
47
|
+
def find_clone
|
48
|
+
raw_clone = find_raw_clone
|
49
|
+
|
50
|
+
if raw_clone
|
51
|
+
config.find_clone(clone_id) || raise(Pagi::CloneNotFount, "clone not tracked")
|
52
|
+
else
|
53
|
+
config.remove_clone(clone_id) && nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def find_raw_clone
|
58
|
+
Array(dblab("clone", "list")).find { |clone| clone["id"] == clone_id }
|
59
|
+
end
|
60
|
+
|
61
|
+
def create_clone
|
62
|
+
raw_clone = dblab("clone", "create", "--id", clone_id, "--username", clone_user, "--password", clone_password)
|
63
|
+
raise "Could not create clone" unless raw_clone
|
64
|
+
|
65
|
+
attributes = {
|
66
|
+
port: raw_clone.dig("db", "port"),
|
67
|
+
password: clone_password,
|
68
|
+
user: clone_user,
|
69
|
+
host: HOSTNAME,
|
70
|
+
dbname: config.dbname
|
71
|
+
}
|
72
|
+
|
73
|
+
config.persist_clone(clone_id, attributes)
|
74
|
+
end
|
75
|
+
|
76
|
+
def psql(clone)
|
77
|
+
start_port_forward(clone.port)
|
78
|
+
|
79
|
+
psql_pid = fork do
|
80
|
+
wait_for_connections(clone.port)
|
81
|
+
exec("psql #{clone.connection_string}")
|
82
|
+
end
|
83
|
+
|
84
|
+
Process.wait(psql_pid)
|
85
|
+
end
|
86
|
+
|
87
|
+
def environment_id
|
88
|
+
environment.fetch(:id)
|
89
|
+
end
|
90
|
+
|
91
|
+
def enviroment_port
|
92
|
+
environment.fetch(:port)
|
93
|
+
end
|
94
|
+
|
95
|
+
def clone_id
|
96
|
+
@clone_id ||= [config.clone_prefix, environment.fetch(:alias)].join("_")
|
97
|
+
end
|
98
|
+
|
99
|
+
def clone_user
|
100
|
+
@clone_user ||= SecureRandom.hex(8)
|
101
|
+
end
|
102
|
+
|
103
|
+
def clone_password
|
104
|
+
@clone_user ||= SecureRandom.hex(16)
|
105
|
+
end
|
106
|
+
|
107
|
+
def dblab(*args, raw: false)
|
108
|
+
executable = config.path || "dblab"
|
109
|
+
|
110
|
+
output = `#{args.unshift(executable).shelljoin}`
|
111
|
+
return output if raw
|
112
|
+
|
113
|
+
JSON.parse(output) unless output.empty?
|
114
|
+
end
|
115
|
+
|
116
|
+
def start_port_forward(port)
|
117
|
+
return if port_open?(port)
|
118
|
+
|
119
|
+
system("ssh -fNTML #{port}:#{HOSTNAME}:#{port} #{config.proxy}")
|
120
|
+
wait_for_connections(port)
|
121
|
+
end
|
122
|
+
|
123
|
+
def stop_port_forwards
|
124
|
+
raw_pids = `ps ax | grep 'ssh -fNTML' | grep '#{config.proxy}' | grep -v grep | awk '{ print $1 }'`
|
125
|
+
|
126
|
+
raw_pids.split.map do |pid|
|
127
|
+
Process.kill("HUP", pid.to_i)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def port_open?(port)
|
132
|
+
!!TCPSocket.new(HOSTNAME, port)
|
133
|
+
rescue Errno::ECONNREFUSED
|
134
|
+
false
|
135
|
+
end
|
136
|
+
|
137
|
+
def wait_for_connections(port)
|
138
|
+
until port_open?(port)
|
139
|
+
sleep 0.02
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
data/lib/pgai/config.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
require "pstore"
|
5
|
+
|
6
|
+
module Pgai
|
7
|
+
class Config
|
8
|
+
Clone = Struct.new(:host, :port, :password, :user, :dbname, keyword_init: true) do
|
9
|
+
def connection_string
|
10
|
+
"'host=#{host} port=#{port} user=#{user} dbname=#{dbname} password=#{password}'"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_accessor :clone_prefix
|
15
|
+
attr_accessor :dbname
|
16
|
+
attr_accessor :access_token
|
17
|
+
attr_accessor :proxy
|
18
|
+
attr_accessor :path
|
19
|
+
|
20
|
+
def self.load
|
21
|
+
new { |config| config.load_from_store }
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.persist(data)
|
25
|
+
new { |config| config.write(data) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
yield self if block_given?
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_from_store
|
33
|
+
store.transaction(true) do
|
34
|
+
self.clone_prefix = store[:clone_prefix]
|
35
|
+
self.dbname = store[:dbname]
|
36
|
+
self.access_token = store[:access_token]
|
37
|
+
self.proxy = store[:proxy]
|
38
|
+
self.path = store[:path]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def write(options)
|
43
|
+
store.transaction do
|
44
|
+
store[:clone_prefix] = options[:prefix]
|
45
|
+
store[:dbname] = options[:dbname]
|
46
|
+
store[:access_token] = options[:token]
|
47
|
+
store[:proxy] = options[:proxy]
|
48
|
+
store[:path] = options[:path]
|
49
|
+
store[:clones] = {}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_clone(id)
|
54
|
+
store.transaction(true) do
|
55
|
+
clones = store[:clones] || {}
|
56
|
+
|
57
|
+
Clone.new(clones[id]) if clones[id]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def remove_clone(id)
|
62
|
+
store.transaction do
|
63
|
+
store[:clones] ||= {}
|
64
|
+
store[:clones] = store[:clones].except(id)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def persist_clone(id, attributes = {})
|
69
|
+
store.transaction do
|
70
|
+
store[:clones] = {}
|
71
|
+
store[:clones].merge!(id => attributes)
|
72
|
+
end
|
73
|
+
|
74
|
+
Clone.new(attributes)
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_env(options = {})
|
78
|
+
store.transaction do
|
79
|
+
store[:environments] ||= {}
|
80
|
+
store[:environments].merge!(options[:alias] => options)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def remove_env(env_alias)
|
85
|
+
store.transaction do
|
86
|
+
store[:environments] ||= {}
|
87
|
+
store[:environments] = store[:environments].except(env_alias)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def enviroments
|
92
|
+
store.transaction(true) do
|
93
|
+
store[:environments] || {}
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def store
|
100
|
+
@store ||= PStore.new(store_path)
|
101
|
+
end
|
102
|
+
|
103
|
+
def store_path
|
104
|
+
@store_path ||= Pathname("~/.config/pgai/config.pstore").expand_path.tap do |path|
|
105
|
+
FileUtils.mkdir_p File.dirname(path)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
data/lib/pgai/version.rb
ADDED
data/lib/pgai.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "pgai/version"
|
4
|
+
require_relative "pgai/config"
|
5
|
+
require_relative "pgai/clone_manager"
|
6
|
+
require_relative "pgai/cli/base"
|
7
|
+
require_relative "pgai/cli/env"
|
8
|
+
require_relative "pgai/cli/main"
|
9
|
+
|
10
|
+
module Pgai
|
11
|
+
class Error < StandardError; end
|
12
|
+
|
13
|
+
class CloneNotFount < Error; end
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pgai
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Marius Bobin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-04-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thor
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.2'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 1.2.1
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.2'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.2.1
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rake
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '13.0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '13.0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rspec
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '3.0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: standard
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.3'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '1.3'
|
75
|
+
description:
|
76
|
+
email:
|
77
|
+
- mbobin@gitlab.com
|
78
|
+
executables:
|
79
|
+
- pgai
|
80
|
+
extensions: []
|
81
|
+
extra_rdoc_files: []
|
82
|
+
files:
|
83
|
+
- MIT-LICENSE
|
84
|
+
- README.md
|
85
|
+
- bin/pgai
|
86
|
+
- lib/pgai.rb
|
87
|
+
- lib/pgai/cli/base.rb
|
88
|
+
- lib/pgai/cli/env.rb
|
89
|
+
- lib/pgai/cli/main.rb
|
90
|
+
- lib/pgai/clone_manager.rb
|
91
|
+
- lib/pgai/config.rb
|
92
|
+
- lib/pgai/version.rb
|
93
|
+
homepage: https://gitlab.com/mbobin/pgai
|
94
|
+
licenses:
|
95
|
+
- MIT
|
96
|
+
metadata: {}
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: 3.0.0
|
106
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
requirements: []
|
112
|
+
rubygems_version: 3.4.1
|
113
|
+
signing_key:
|
114
|
+
specification_version: 4
|
115
|
+
summary: CLI wrapper for postgres.ai thin clones
|
116
|
+
test_files: []
|