gretl 1.0.4 → 1.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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/gretl.rb +213 -1
  3. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1656965332fced86693ad3f2c07fdd3ddf9a7456ee6985109813e5d92226523f
4
- data.tar.gz: d5c7cc8aac92b0d20d7da86f7b0e258e336445fa38cbf78e4560b3a54d5fe4d3
3
+ metadata.gz: 4c766301a9c384e993a02eebdb5a67d600f457de87ac78baba481f3fcc8fca10
4
+ data.tar.gz: a025513173739016a89e6eb8b8413719bb6159d0ecae5c2af4a6bb726ec21999
5
5
  SHA512:
6
- metadata.gz: fa39a4f95613e614c4ea3ba13eed47d9a5ce770fba0416f3ed8b2718bebcdab2b72f7acd24d715eead4a112a8d70609ad78a797169b55f5ca2fa969126609db4
7
- data.tar.gz: 664e92a0787ac6c3b81421372475234f904346c71862b862f09653ac173383de5d403382552add34518c9d76cbb023696c99a9b9403c8330c37195e110029097
6
+ metadata.gz: a07ef48d121f1fa1daa154533974aee8e44042b8bc90a632d4327c22d4c5de5baed59097021794eb20591447340c47f9d3156f3a4ddcef6529d77e82fa7fbd22
7
+ data.tar.gz: 4aaa684b0aaa0aa8c09f907a07c3a559dd1f96191a35fbb25f2267207dfcf008db9d4497f7dd95c955b6c55c376997c6136b1329bbf14c462ead7705d4e8c3b7
data/lib/gretl.rb CHANGED
@@ -155,6 +155,218 @@ module Gretl
155
155
  end
156
156
  end
157
157
 
158
- # Default client
158
+ CLOUD_API = "https://api.gretl.dev"
159
+
160
+ class K8sClient
161
+ def initialize(token)
162
+ @token = token
163
+ end
164
+
165
+ def clusters
166
+ _cloud_get("/k8s/clusters")
167
+ end
168
+
169
+ def workloads(cluster_id: nil)
170
+ path = cluster_id ? "/k8s/clusters/#{cluster_id}/workloads" : "/k8s/workloads"
171
+ _cloud_get(path)
172
+ end
173
+
174
+ def sync(cluster_id)
175
+ _cloud_post("/k8s/clusters/#{cluster_id}/sync")
176
+ end
177
+
178
+ def sleep(workload_id)
179
+ _cloud_post("/k8s/workloads/#{workload_id}/sleep")
180
+ end
181
+
182
+ def wake(workload_id)
183
+ _cloud_post("/k8s/workloads/#{workload_id}/wake")
184
+ end
185
+
186
+ def tether(workload_id, container_port:, label: nil)
187
+ body = { container_port: container_port }
188
+ body[:label] = label if label
189
+ _cloud_post("/k8s/workloads/#{workload_id}/tether", body)
190
+ end
191
+
192
+ def detach(tether_id)
193
+ _cloud_post("/k8s/tethers/#{tether_id}/deactivate")
194
+ end
195
+
196
+ def tethers
197
+ _cloud_get("/k8s/tethers")
198
+ end
199
+
200
+ def auto_sleep(workload_id)
201
+ _cloud_get("/k8s/workloads/#{workload_id}/auto-sleep")
202
+ end
203
+
204
+ def set_auto_sleep(workload_id, enabled:, idle_timeout_minutes:)
205
+ _cloud_put("/k8s/workloads/#{workload_id}/auto-sleep",
206
+ { auto_sleep_enabled: enabled, idle_timeout_minutes: idle_timeout_minutes })
207
+ end
208
+
209
+ private
210
+
211
+ def _cloud_get(path)
212
+ uri = URI("#{CLOUD_API}#{path}")
213
+ req = Net::HTTP::Get.new(uri, "Authorization" => "Bearer #{@token}")
214
+ _cloud_call(uri, req)
215
+ end
216
+
217
+ def _cloud_post(path, body = nil)
218
+ uri = URI("#{CLOUD_API}#{path}")
219
+ req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{@token}", "Content-Type" => "application/json")
220
+ req.body = body.to_json if body
221
+ _cloud_call(uri, req)
222
+ end
223
+
224
+ def _cloud_put(path, body)
225
+ uri = URI("#{CLOUD_API}#{path}")
226
+ req = Net::HTTP::Put.new(uri, "Authorization" => "Bearer #{@token}", "Content-Type" => "application/json")
227
+ req.body = body.to_json
228
+ _cloud_call(uri, req)
229
+ end
230
+
231
+ def _cloud_call(uri, req)
232
+ http = Net::HTTP.new(uri.host, uri.port)
233
+ http.use_ssl = uri.scheme == "https"
234
+ http.open_timeout = 10
235
+ http.read_timeout = 10
236
+ res = http.request(req)
237
+ raise "Gretl API error #{res.code}: #{res.body}" unless res.is_a?(Net::HTTPSuccess)
238
+ JSON.parse(res.body, symbolize_names: true)
239
+ rescue Errno::ECONNREFUSED => e
240
+ raise "Cannot reach Gretl API: #{e}"
241
+ end
242
+ end
243
+
244
+ class Client
245
+ def initialize(token: nil)
246
+ _token = token || ENV.fetch("GR_TOKEN", "")
247
+ @k8s = K8sClient.new(_token)
248
+ end
249
+
250
+ attr_reader :k8s
251
+
252
+ def ports = Gretl._fetch_ports
253
+ def port(query) = Gretl._resolve(query)
254
+ def active = Gretl._fetch_ports.select(&:active)
255
+ def inactive = Gretl._fetch_ports.reject(&:active)
256
+
257
+ def start(query)
258
+ matches = Gretl._resolve(query)
259
+ raise ArgumentError, "No port found matching #{query.inspect}" if matches.empty?
260
+ matches.each do |p|
261
+ Gretl._request(:post, "/ports/#{p.port}/start") if !p.active && p.has_command
262
+ end
263
+ matches
264
+ end
265
+
266
+ def stop(query)
267
+ matches = Gretl._resolve(query)
268
+ raise ArgumentError, "No port found matching #{query.inspect}" if matches.empty?
269
+ matches.each do |p|
270
+ Gretl._request(:post, "/ports/#{p.port}/stop") if p.active
271
+ end
272
+ matches
273
+ end
274
+
275
+ def start_group(group)
276
+ matches = Gretl._fetch_ports.select { |p| p.group&.downcase == group.downcase }
277
+ raise ArgumentError, "No ports found in group #{group.inspect}" if matches.empty?
278
+ matches.each { |p| Gretl._request(:post, "/ports/#{p.port}/start") if !p.active && p.has_command }
279
+ matches
280
+ end
281
+
282
+ def stop_group(group)
283
+ matches = Gretl._fetch_ports.select { |p| p.group&.downcase == group.downcase }
284
+ raise ArgumentError, "No ports found in group #{group.inspect}" if matches.empty?
285
+ matches.each { |p| Gretl._request(:post, "/ports/#{p.port}/stop") if p.active }
286
+ matches
287
+ end
288
+
289
+ def status_group(group)
290
+ matches = Gretl._fetch_ports.select { |p| p.group&.downcase == group.downcase }
291
+ raise ArgumentError, "No ports found in group #{group.inspect}" if matches.empty?
292
+ matches
293
+ end
294
+
295
+ def remove(query)
296
+ matches = Gretl._resolve(query)
297
+ raise ArgumentError, "No port found matching #{query.inspect}" if matches.empty?
298
+ matches.each { |p| Gretl._request(:delete, "/ports/#{p.port}") }
299
+ matches
300
+ end
301
+
302
+ def wait_for_active(query, timeout: 30, interval: 0.5)
303
+ deadline = Time.now + timeout
304
+ while Time.now < deadline
305
+ Gretl._resolve(query).each { |p| return p if p.active }
306
+ sleep interval
307
+ end
308
+ raise "Port matching #{query.inspect} did not become active within #{timeout}s"
309
+ end
310
+
311
+ def add(port, name: nil, cmd: nil, group: nil, cwd: nil)
312
+ Gretl._ensure_daemon
313
+ uri = URI("#{Gretl::BASE}/ports")
314
+ body = { port: port }
315
+ body[:name] = name if name
316
+ body[:cmd] = cmd if cmd
317
+ body[:group] = group if group
318
+ body[:cwd] = cwd if cwd
319
+ http = Net::HTTP.new(uri.host, uri.port)
320
+ req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json")
321
+ req.body = body.to_json
322
+ res = http.request(req)
323
+ data = JSON.parse(res.body, symbolize_names: true)
324
+ Gretl::PortInfo.new(**data.slice(*Gretl::PortInfo.members))
325
+ end
326
+
327
+ def wait_for_inactive(query, timeout: 30, interval: 0.5)
328
+ deadline = Time.now + timeout
329
+ while Time.now < deadline
330
+ Gretl._resolve(query).each { |p| return p if p.stopped? }
331
+ sleep interval
332
+ end
333
+ raise "Port matching #{query.inspect} did not become inactive within #{timeout}s"
334
+ end
335
+
336
+ # ── Agent gateway ─────────────────────────────────────────────────────────
337
+
338
+ def agent_list
339
+ Gretl._fetch_ports.select { |p| p.group&.downcase == "agents" }
340
+ end
341
+
342
+ def agent_spinup(name, cmd: nil, port: nil)
343
+ unless port
344
+ used = Gretl._fetch_ports.map(&:port).to_set
345
+ port = 8100
346
+ port += 1 while used.include?(port) && port <= 8200
347
+ raise "No available ports in the agent range (8100–8200)" if port > 8200
348
+ end
349
+ add(port, name: name, cmd: cmd || "", group: "Agents")
350
+ Gretl._request(:post, "/ports/#{port}/start")
351
+ raw = Gretl._request(:get, "/ports/#{port}")
352
+ Gretl::PortInfo.new(**raw.slice(*Gretl::PortInfo.members))
353
+ end
354
+
355
+ def agent_spindown(name)
356
+ match = agent_list.find { |p| p.name.downcase == name.downcase }
357
+ raise ArgumentError, "No agent found with name #{name.inspect}" unless match
358
+ Gretl._request(:post, "/ports/#{match.port}/stop") if match.active
359
+ Gretl._request(:delete, "/ports/#{match.port}")
360
+ end
361
+
362
+ def agent_logs(name, lines: 50)
363
+ match = agent_list.find { |p| p.name.downcase == name.downcase }
364
+ raise ArgumentError, "No agent found with name #{name.inspect}" unless match
365
+ raw = Gretl._request(:get, "/ports/#{match.port}/logs?lines=#{lines}")
366
+ raw[:lines] || []
367
+ end
368
+ end
369
+
370
+ # Default client — token read from GR_TOKEN env var
159
371
  PA = Client.new
160
372
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gretl
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gretl
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-15 00:00:00.000000000 Z
11
+ date: 2026-06-09 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email: