bsdkrun 0.4.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e78b89f68872f94abe500dba9bc0d69bd2b8ff0488745bb1f7e07ef2658f8a28
4
- data.tar.gz: 1effefa03711b90030219d9456edbb6df361591184aa9bb7f01c06e87ad582e2
3
+ metadata.gz: d1b2ffdacfec58cee3714b8ca96ecc133604655d423e264444d8f41ea88393e5
4
+ data.tar.gz: 848ec7c03c5489a41daf404135c3bbfdcfb126449ecb712ddf920ea7377bc822
5
5
  SHA512:
6
- metadata.gz: ce3f123068f4c08775e7308513b727182ece1ce14737de2cc874ed050be8e19979e82c902703202f72a162222d0b12363a6aefc5b9441eeab34038976ed28a1c
7
- data.tar.gz: 8eb660e5db46bf1fc617b250f76c55f41f75943f2bc71026994790e812c000ae90aa63384956938e7a5bda406d8f59912ab36ac7f5a5029a615738a1ccb4dca3
6
+ metadata.gz: a86228909ae198caab0e630bac039179fc115317118f321a1aa193683ee6fb10ccfdc56ae1d05d42cad3ebad712897b5979c0b3b75cd0fe9b64bef0406d1d5ae
7
+ data.tar.gz: aef36636aef92124b1c78695648c929bfee4f60e134b9ef0a2d5588693827994e8023f62c91f39cd2d1a19c74e7e1a6a3d99a99c2aca9df51ba53bf03022368e
data/README.md CHANGED
@@ -300,6 +300,37 @@ the new machine's id. `run_solo5` boots a MirageOS unikernel under the
300
300
  `stop`/`start`/`remove`/`update`/`commit` return a
301
301
  `CommandResult` (`exit_code`, `stdout`, `stderr`).
302
302
 
303
+ ### Snapshots
304
+
305
+ A snapshot is a **copy-on-write clone of a machine's disk state** — instant to
306
+ take, free until the two sides diverge. `branch` boots a new machine from one
307
+ (or from a machine, which is snapshotted first); `restore`/`rollback` put one
308
+ back, leaving the machine stopped. A BSD guest is powered off to snapshot it:
309
+ a mounted UFS cannot be cloned consistently.
310
+
311
+ ```ruby
312
+ snap = client.snapshot(machine_id, name: "before-upgrade")
313
+ client.snapshots(machine: machine_id) # newest first
314
+ branch_id = client.branch(snap.name, name: "web-test")
315
+ client.restore(machine_id, snap.name) # or client.rollback(machine_id)
316
+ client.remove_snapshots(snap.name)
317
+ ```
318
+
319
+ ### Docker
320
+
321
+ bsdkrun runs one `docker:dind` microVM and serves its API on a host unix
322
+ socket, so the host's own `docker` CLI drives the same engine these calls do.
323
+ Starting is idempotent — the VM has a fixed name, so it resumes rather than
324
+ creating a second.
325
+
326
+ ```ruby
327
+ status = client.docker_start(cpus: 4, mem: 4096) # or just docker_status
328
+ puts status.socket
329
+ client.docker_containers.each { |c| puts "#{c.name} #{c.state} #{c.ports}" }
330
+ client.docker_container("restart", "web")
331
+ puts client.docker_logs("web", tail: 50)
332
+ ```
333
+
303
334
  For a live terminal instead of a one-shot `exec`, use `shell`:
304
335
 
305
336
  ```ruby
@@ -35,10 +35,16 @@ module Bsdkrun
35
35
  # +web/src/lib/api.ts+'s +MACHINE_FIELDS+ fragment exactly.
36
36
  MACHINE_FIELDS = <<~GQL.freeze
37
37
  id name image kind command status running exitCode pid detached
38
- cpus mem volume stateDir createdAt finishedAt network netIp
38
+ cpus mem volume stateDir createdAt finishedAt network netIp origin
39
39
  ports { bind host guest }
40
40
  GQL
41
41
 
42
+ # The +Snapshot+ selection, likewise shared by every snapshot document.
43
+ SNAPSHOT_FIELDS = <<~GQL.freeze
44
+ id name machineId machineName kind image path parent description
45
+ cpus mem size createdAt ports { bind host guest }
46
+ GQL
47
+
42
48
  # @return [String] the GraphQL endpoint URL (normalized).
43
49
  attr_reader :url
44
50
 
@@ -242,6 +248,269 @@ module Bsdkrun
242
248
  )
243
249
  end
244
250
 
251
+ # ---- ai agents -------------------------------------------------------------
252
+ #
253
+ # A sandbox is a machine, so its terminal is the ordinary {#shell} with the
254
+ # argv {#ai_shell_command} returns.
255
+
256
+ AI_AGENT_FIELDS = "id label flavor description installed running"
257
+ AI_SESSION_FIELDS = "id name agent running workspace createdAt"
258
+
259
+ # The coding agents, and whether each one's sandbox image is built.
260
+ # @return [Array<AiAgent>]
261
+ def ai_agents
262
+ data = request("{ aiAgents { #{AI_AGENT_FIELDS} } }")
263
+ (data["aiAgents"] || []).map { |a| AiAgent.from_graphql(a) }
264
+ end
265
+
266
+ # Agent sandboxes, newest first.
267
+ # @return [Array<AiSession>]
268
+ def ai_sessions
269
+ data = request("{ aiSessions { #{AI_SESSION_FIELDS} } }")
270
+ (data["aiSessions"] || []).map { |s| AiSession.from_graphql(s) }
271
+ end
272
+
273
+ # Start (or reuse) a sandbox; returns its machine id.
274
+ #
275
+ # @param agent [String] +claude+, +codex+, ...
276
+ # @param workspace [String, nil] a directory **on the engine's host**.
277
+ # @param new [Boolean] boot a second sandbox against the same saved login.
278
+ # @return [String]
279
+ def ai_start(agent, cpus: nil, mem: nil, workspace: nil, new: false)
280
+ data = request(
281
+ "mutation($input:AiStartInput!){ aiStart(input:$input) }",
282
+ { input: { agent: agent, cpus: cpus, mem: mem,
283
+ workspace: workspace, new: new } }
284
+ )
285
+ data["aiStart"].to_s
286
+ end
287
+
288
+ # The argv that starts the agent's TUI — pass it to {#shell}.
289
+ # @return [Array<String>]
290
+ def ai_shell_command(agent, machine_id)
291
+ data = request(
292
+ "query($agent:String!,$machineId:String!){ " \
293
+ "aiShellCommand(agent:$agent, machineId:$machineId) }",
294
+ { agent: agent, machineId: machine_id }
295
+ )
296
+ Array(data["aiShellCommand"])
297
+ end
298
+
299
+ # Stop an agent's sandboxes. Its saved login survives.
300
+ # @return [CommandResult]
301
+ def ai_stop(agent)
302
+ run_command_mutation(
303
+ "aiStop",
304
+ "mutation($agent:String!){ aiStop(agent:$agent){ exitCode stdout stderr } }",
305
+ { agent: agent }
306
+ )
307
+ end
308
+
309
+ # Remove an agent's sandboxes, and unless +keep_home+ its saved login too.
310
+ # @return [CommandResult]
311
+ def ai_remove(agent, keep_home: false)
312
+ run_command_mutation(
313
+ "aiRemove",
314
+ "mutation($agent:String!,$keepHome:Boolean!){ " \
315
+ "aiRemove(agent:$agent, keepHome:$keepHome){ exitCode stdout stderr } }",
316
+ { agent: agent, keepHome: keep_home }
317
+ )
318
+ end
319
+
320
+ # ---- docker --------------------------------------------------------------
321
+ #
322
+ # bsdkrun runs one +docker:dind+ microVM and serves its API on a host unix
323
+ # socket, so these drive the same engine the host's +docker+ CLI does.
324
+
325
+ DOCKER_STATUS_FIELDS = <<~GQL.freeze
326
+ running machineId machineRunning socket socketReady apiPort version
327
+ containers images mounts disk diskSize
328
+ GQL
329
+
330
+ DOCKER_CONTAINER_FIELDS = "id name image command state status ports created"
331
+
332
+ # Is the Docker engine up, and where is its socket?
333
+ # @return [DockerStatus]
334
+ def docker_status
335
+ data = request("{ dockerStatus { #{DOCKER_STATUS_FIELDS} } }")
336
+ DockerStatus.from_graphql(data["dockerStatus"])
337
+ end
338
+
339
+ # Containers in the engine.
340
+ # @param all [Boolean] include stopped ones (default true).
341
+ # @return [Array<DockerContainer>]
342
+ def docker_containers(all: true)
343
+ data = request(
344
+ "query($all:Boolean!){ dockerContainers(all:$all){ #{DOCKER_CONTAINER_FIELDS} } }",
345
+ { all: all }
346
+ )
347
+ (data["dockerContainers"] || []).map { |c| DockerContainer.from_graphql(c) }
348
+ end
349
+
350
+ # Start (or resume) the engine, returning its status once it answers.
351
+ #
352
+ # Idempotent: the VM has a fixed name, so this resumes the existing one
353
+ # rather than creating a second.
354
+ #
355
+ # @param cpus [Integer, nil]
356
+ # @param mem [Integer, nil]
357
+ # @param mounts [Array<String>] host dirs to share, +PATH+ or +HOST:GUEST+.
358
+ # @param no_home [Boolean] do not share +$HOME+ (shared by default).
359
+ # @param publish_bind [String, nil] +mirror+ (default) or a fixed address.
360
+ # @param disk_size [String, nil] a dedicated image store, e.g. +60G+.
361
+ # @return [DockerStatus]
362
+ def docker_start(cpus: nil, mem: nil, mounts: [], no_home: false,
363
+ publish_bind: nil, disk_size: nil)
364
+ data = request(
365
+ "mutation($input:DockerStartInput!){ dockerStart(input:$input){ " \
366
+ "#{DOCKER_STATUS_FIELDS} } }",
367
+ { input: { cpus: cpus, mem: mem, mounts: Array(mounts), noHome: no_home,
368
+ publishBind: publish_bind, diskSize: disk_size } }
369
+ )
370
+ DockerStatus.from_graphql(data["dockerStart"])
371
+ end
372
+
373
+ # Stop the engine. Images and containers stay on its disk.
374
+ # @return [CommandResult]
375
+ def docker_stop
376
+ run_command_mutation(
377
+ "dockerStop",
378
+ "mutation{ dockerStop{ exitCode stdout stderr } }",
379
+ {}
380
+ )
381
+ end
382
+
383
+ # start | stop | restart | kill | pause | unpause | rm.
384
+ # @param action [String]
385
+ # @param ids [String, Array<String>]
386
+ # @return [CommandResult]
387
+ def docker_container(action, ids)
388
+ run_command_mutation(
389
+ "dockerContainer",
390
+ "mutation($action:String!,$ids:[String!]!){ " \
391
+ "dockerContainer(action:$action, ids:$ids){ exitCode stdout stderr } }",
392
+ { action: action, ids: Array(ids) }
393
+ )
394
+ end
395
+
396
+ # One container's logs (stdout+stderr, most recent +tail+ lines).
397
+ # @param id [String]
398
+ # @param tail [Integer]
399
+ # @return [String]
400
+ def docker_logs(id, tail: 200)
401
+ data = request(
402
+ "query($id:String!,$tail:Int!){ dockerContainerLogs(id:$id, tail:$tail) }",
403
+ { id: id, tail: tail }
404
+ )
405
+ data["dockerContainerLogs"].to_s
406
+ end
407
+
408
+ # ---- snapshots ---------------------------------------------------------
409
+ #
410
+ # A snapshot is a copy-on-write clone of a machine's disk state: instant to
411
+ # take, free until the two sides diverge. {#branch} boots a new machine
412
+ # from one; {#restore}/{#rollback} put one back.
413
+
414
+ # List snapshots, newest first.
415
+ # @param machine [String, nil] only this machine's, when given.
416
+ # @return [Array<SnapshotInfo>]
417
+ def snapshots(machine: nil)
418
+ data = request(
419
+ "query($machine:String){ snapshots(machine:$machine){ #{SNAPSHOT_FIELDS} } }",
420
+ { machine: machine }
421
+ )
422
+ (data["snapshots"] || []).map { |s| SnapshotInfo.from_graphql(s) }
423
+ end
424
+
425
+ # Capture a machine's disk state.
426
+ #
427
+ # A BSD guest is powered off first — a mounted UFS cannot be cloned
428
+ # consistently — so the machine is left stopped; {#start} brings it back.
429
+ #
430
+ # @param id [String]
431
+ # @param name [String, nil] defaults to +<machine>-<n>+.
432
+ # @param description [String]
433
+ # @return [SnapshotInfo]
434
+ def snapshot(id, name: nil, description: "")
435
+ data = request(
436
+ "mutation($id:String!,$name:String,$description:String!){ " \
437
+ "snapshotMachine(id:$id, name:$name, description:$description){ #{SNAPSHOT_FIELDS} } }",
438
+ { id: id, name: name, description: description }
439
+ )
440
+ SnapshotInfo.from_graphql(data["snapshotMachine"])
441
+ end
442
+
443
+ # Delete snapshots and their data. Machines branched from them are
444
+ # unaffected.
445
+ # @param names [String, Array<String>]
446
+ # @return [CommandResult]
447
+ def remove_snapshots(names)
448
+ run_command_mutation(
449
+ "removeSnapshots",
450
+ "mutation($names:[String!]!){ removeSnapshots(names:$names){ exitCode stdout stderr } }",
451
+ { names: Array(names) }
452
+ )
453
+ end
454
+
455
+ # Put a machine's disk state back to one of its snapshots.
456
+ #
457
+ # +force+ stops the machine first (it holds the very files being
458
+ # replaced); +backup+ snapshots the state being overwritten, which is a
459
+ # CoW clone and therefore free. The machine is left stopped.
460
+ #
461
+ # @param id [String]
462
+ # @param snapshot [String]
463
+ # @param force [Boolean]
464
+ # @param backup [Boolean]
465
+ # @return [CommandResult]
466
+ def restore(id, snapshot, force: true, backup: true)
467
+ run_command_mutation(
468
+ "restoreMachine",
469
+ "mutation($id:String!,$snapshot:String!,$force:Boolean!,$backup:Boolean!){ " \
470
+ "restoreMachine(id:$id, snapshot:$snapshot, force:$force, backup:$backup){ " \
471
+ "exitCode stdout stderr } }",
472
+ { id: id, snapshot: snapshot, force: force, backup: backup }
473
+ )
474
+ end
475
+
476
+ # Restore a machine to its most recent snapshot.
477
+ # @param id [String]
478
+ # @param force [Boolean]
479
+ # @param backup [Boolean]
480
+ # @return [CommandResult]
481
+ def rollback(id, force: true, backup: true)
482
+ run_command_mutation(
483
+ "rollbackMachine",
484
+ "mutation($id:String!,$force:Boolean!,$backup:Boolean!){ " \
485
+ "rollbackMachine(id:$id, force:$force, backup:$backup){ exitCode stdout stderr } }",
486
+ { id: id, force: force, backup: backup }
487
+ )
488
+ end
489
+
490
+ # Boot a NEW machine from a snapshot — or from a machine, which is
491
+ # snapshotted first — and return the new machine's id.
492
+ #
493
+ # The state is cloned, never booted in place, so the source is untouched
494
+ # and one snapshot can be branched any number of times. With no +ports+,
495
+ # the snapshot's own forwards are inherited, with any host port that is
496
+ # already taken swapped for a free one.
497
+ #
498
+ # @param snapshot [String] snapshot name/id, or a machine id.
499
+ # @param name [String, nil]
500
+ # @param cpus [Integer, nil]
501
+ # @param mem [Integer, nil]
502
+ # @param ports [Array<String>]
503
+ # @param no_ports [Boolean]
504
+ # @return [String] the new machine's id.
505
+ def branch(snapshot, name: nil, cpus: nil, mem: nil, ports: [], no_ports: false)
506
+ data = request(
507
+ "mutation($input:BranchInput!){ branchSnapshot(input:$input) }",
508
+ { input: { snapshot: snapshot, name: name, cpus: cpus, mem: mem,
509
+ ports: Array(ports), noPorts: no_ports } }
510
+ )
511
+ data["branchSnapshot"].to_s
512
+ end
513
+
245
514
  # One-shot console log fetch. Use {#follow_logs} to stream instead.
246
515
  # @param id [String]
247
516
  # @param boot [Boolean] bsdkrun's own boot log instead of the guest console.
data/lib/bsdkrun/types.rb CHANGED
@@ -34,7 +34,7 @@ module Bsdkrun
34
34
  SandboxInfo = Data.define(
35
35
  :id, :name, :image, :kind, :command, :status, :running, :exit_code,
36
36
  :pid, :detached, :cpus, :mem, :volume, :state_dir, :network, :net_ip,
37
- :created_at, :finished_at, :ports
37
+ :created_at, :finished_at, :ports, :origin
38
38
  ) do
39
39
  # Map a +ps --json+ row (String keys) to a typed instance.
40
40
  # @param row [Hash]
@@ -60,7 +60,8 @@ module Bsdkrun
60
60
  net_ip: row["net_ip"],
61
61
  created_at: row["created_at"].to_i,
62
62
  finished_at: to_i_or_nil(row["finished_at"]),
63
- ports: (row["ports"] || []).map { |p| PortForward.from_row(p) }
63
+ ports: (row["ports"] || []).map { |p| PortForward.from_row(p) },
64
+ origin: row["origin"]
64
65
  )
65
66
  end
66
67
 
@@ -96,11 +97,190 @@ module Bsdkrun
96
97
  net_ip: m["netIp"],
97
98
  created_at: m["createdAt"].to_i,
98
99
  finished_at: to_i_or_nil(m["finishedAt"]),
99
- ports: (m["ports"] || []).map { |p| PortForward.from_row(p) }
100
+ ports: (m["ports"] || []).map { |p| PortForward.from_row(p) },
101
+ origin: m["origin"]
100
102
  )
101
103
  end
102
104
  end
103
105
 
106
+ # A machine snapshot: one machine's disk state, captured under a name.
107
+ #
108
+ # A copy-on-write clone rather than a memory image — the files the guest
109
+ # wrote, not what it was executing. {Bsdkrun::Client#branch} boots a new
110
+ # machine from one; {Bsdkrun::Client#restore} puts one back.
111
+ SnapshotInfo = Data.define(
112
+ :id, :name, :machine_id, :machine_name, :kind, :image, :path, :parent,
113
+ :description, :cpus, :mem, :ports, :size, :created_at
114
+ ) do
115
+ # Map a GraphQL +Snapshot+ (camelCase) to a typed instance.
116
+ # @param s [Hash]
117
+ # @return [SnapshotInfo]
118
+ def self.from_graphql(s)
119
+ new(
120
+ id: s["id"].to_s,
121
+ name: s["name"].to_s,
122
+ machine_id: s["machineId"].to_s,
123
+ machine_name: (s["machineName"] || "").to_s,
124
+ kind: s["kind"].to_s,
125
+ image: (s["image"] || "").to_s,
126
+ path: (s["path"] || "").to_s,
127
+ parent: s["parent"],
128
+ description: (s["description"] || "").to_s,
129
+ cpus: s["cpus"].to_i,
130
+ mem: s["mem"].to_i,
131
+ ports: (s["ports"] || []).map { |p| PortForward.from_row(p) },
132
+ size: s["size"],
133
+ created_at: s["createdAt"].to_i
134
+ )
135
+ end
136
+
137
+ # Map a +bsdkrun snapshots --json+ row (snake_case).
138
+ # @param row [Hash]
139
+ # @return [SnapshotInfo]
140
+ def self.from_row(row)
141
+ new(
142
+ id: row["id"].to_s,
143
+ name: row["name"].to_s,
144
+ machine_id: row["machine_id"].to_s,
145
+ machine_name: (row["machine_name"] || "").to_s,
146
+ kind: row["kind"].to_s,
147
+ image: (row["image"] || "").to_s,
148
+ path: (row["path"] || "").to_s,
149
+ parent: row["parent"],
150
+ description: (row["description"] || "").to_s,
151
+ cpus: row["cpus"].to_i,
152
+ mem: row["mem"].to_i,
153
+ ports: (row["ports"] || []).map { |p| PortForward.from_row(p) },
154
+ size: row["size"],
155
+ created_at: row["created_at"].to_i
156
+ )
157
+ end
158
+ end
159
+
160
+ # A coding agent bsdkrun can sandbox.
161
+ #
162
+ # Each runs in a disposable microVM with a persistent login, a shared skills
163
+ # store, and only the folder you choose to share.
164
+ AiAgent = Data.define(
165
+ :id, :label, :flavor, :description, :installed, :running
166
+ ) do
167
+ # @param a [Hash]
168
+ # @return [AiAgent]
169
+ def self.from_graphql(a)
170
+ new(
171
+ id: a["id"].to_s,
172
+ label: a["label"].to_s,
173
+ flavor: a["flavor"].to_s,
174
+ description: (a["description"] || "").to_s,
175
+ installed: !!a["installed"],
176
+ running: a["running"].to_i
177
+ )
178
+ end
179
+
180
+ class << self
181
+ alias from_row from_graphql
182
+ end
183
+ end
184
+
185
+ # One agent sandbox. It is a machine, so +logs+/+stop+ work on +id+.
186
+ AiSession = Data.define(
187
+ :id, :name, :agent, :running, :workspace, :created_at
188
+ ) do
189
+ # @param s [Hash]
190
+ # @return [AiSession]
191
+ def self.from_graphql(s)
192
+ new(
193
+ id: s["id"].to_s,
194
+ name: s["name"].to_s,
195
+ agent: s["agent"].to_s,
196
+ running: !!s["running"],
197
+ workspace: s["workspace"],
198
+ created_at: (s["createdAt"] || s["created_at"]).to_i
199
+ )
200
+ end
201
+
202
+ class << self
203
+ alias from_row from_graphql
204
+ end
205
+ end
206
+
207
+ # The Docker engine VM: whether it is up, and how to reach it.
208
+ #
209
+ # bsdkrun runs one +docker:dind+ microVM and serves its API on a host unix
210
+ # socket, so the host's own +docker+ CLI drives the same engine.
211
+ DockerStatus = Data.define(
212
+ :running, :machine_id, :machine_running, :socket, :socket_ready, :api_port,
213
+ :version, :containers, :images, :mounts, :disk, :disk_size
214
+ ) do
215
+ # @param s [Hash] a GraphQL +DockerStatus+ (camelCase).
216
+ # @return [DockerStatus]
217
+ def self.from_graphql(s)
218
+ new(
219
+ running: !!s["running"],
220
+ machine_id: s["machineId"],
221
+ machine_running: !!s["machineRunning"],
222
+ socket: s["socket"].to_s,
223
+ socket_ready: !!s["socketReady"],
224
+ api_port: to_i_or_nil(s["apiPort"]),
225
+ version: s["version"],
226
+ containers: to_i_or_nil(s["containers"]),
227
+ images: to_i_or_nil(s["images"]),
228
+ mounts: Array(s["mounts"]),
229
+ disk: s["disk"],
230
+ disk_size: to_i_or_nil(s["diskSize"])
231
+ )
232
+ end
233
+
234
+ # @param row [Hash] a +bsdkrun docker status --json+ row (snake_case).
235
+ # @return [DockerStatus]
236
+ def self.from_row(row)
237
+ new(
238
+ running: !!row["running"],
239
+ machine_id: row["machine_id"],
240
+ machine_running: !!row["machine_running"],
241
+ socket: row["socket"].to_s,
242
+ socket_ready: !!row["socket_ready"],
243
+ api_port: to_i_or_nil(row["api_port"]),
244
+ version: row["version"],
245
+ containers: to_i_or_nil(row["containers"]),
246
+ images: to_i_or_nil(row["images"]),
247
+ mounts: Array(row["mounts"]),
248
+ disk: row["disk"],
249
+ disk_size: to_i_or_nil(row["disk_size"])
250
+ )
251
+ end
252
+ end
253
+
254
+ # A container in the Docker engine VM — a trimmed +docker ps+ row.
255
+ DockerContainer = Data.define(
256
+ :id, :name, :image, :command, :state, :status, :ports, :created
257
+ ) do
258
+ # @return [Boolean] whether the container is up.
259
+ def running?
260
+ state == "running"
261
+ end
262
+
263
+ # Both the GraphQL object and +docker ps --json+ use these field names.
264
+ # @param c [Hash]
265
+ # @return [DockerContainer]
266
+ def self.from_graphql(c)
267
+ new(
268
+ id: c["id"].to_s,
269
+ name: c["name"].to_s,
270
+ image: c["image"].to_s,
271
+ command: (c["command"] || "").to_s,
272
+ state: c["state"].to_s,
273
+ status: c["status"].to_s,
274
+ ports: Array(c["ports"]),
275
+ created: c["created"].to_i
276
+ )
277
+ end
278
+
279
+ class << self
280
+ alias from_row from_graphql
281
+ end
282
+ end
283
+
104
284
  # An image as reported by +bsdkrun images --json+.
105
285
  ImageInfo = Data.define(:id, :reference, :digest, :size, :rootfs, :created_at) do
106
286
  # @param row [Hash]
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Bsdkrun
4
4
  # The bsdkrun Ruby SDK version.
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bsdkrun
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tsiry Sandratraina