captive-cli 1.4.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 377f77798282f817bcd1632482c5075ca10f8c65f1b5ce9abc4e7d3f1aeae2ee
4
- data.tar.gz: ef1dc980b18c772c703a7b94ee35a3c35049b22d84ffeca742ee5ce569a32f7e
3
+ metadata.gz: a31d2e7433e7b31ac59e131ba6a4ffd55732c3e71641fd5e22a45340e8f3f814
4
+ data.tar.gz: 72b86e63fd6a755f22fab06fd60f1eff32fab9dd8f2c1dfff7a485b11e1a0cd2
5
5
  SHA512:
6
- metadata.gz: e058db4108f3da4be9dd9c6e5699f1cdb5419463cfe93300cd96dbd8bf612894f23bf97fab095480edc22e70a9c98f4e172e56edb78e8c1108bac3c66b1918be
7
- data.tar.gz: 8f199adf232216645878442735570e5ee04a70dd4a8f43031bd0e24d67c12f7af7f5ab8ba7e4de2accfb6143a5801f09fa5b173cf80ed88a9e43ceff5d31a3f2
6
+ metadata.gz: a4b15fc087ba7cc009d657b7ce9d0f5738b141d423797d25ab435c43e2aa77442b408db411772dbf1cc3a7b7244c087c56668d6d89c0ccd418fd18a2ce41d6e5
7
+ data.tar.gz: 688bfa7d022d5a4e7605624741153fbb84c0aa9ba90c677035acb87b6935d1febec027421ec3762360ce6fda3ea9c840a9d52f48b816da57a8feffb02caa3da4
@@ -25,6 +25,7 @@ module Captive
25
25
  def env_api = @env_api ||= Dashboard::EnvVarsResource.new(api: dashboard)
26
26
  def crons_api = @crons_api ||= Dashboard::ScheduledTasksResource.new(api: dashboard)
27
27
  def domains_api = @domains_api ||= Dashboard::DomainsResource.new(api: dashboard)
28
+ def addons_api = @addons_api ||= Dashboard::AddonsResource.new(api: dashboard)
28
29
 
29
30
  def active_kubeconfig
30
31
  env_kc = ENV["KUBECONFIG"]
data/lib/captive/cli.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "commands/apps_commands"
7
7
  require_relative "commands/env_commands"
8
8
  require_relative "commands/crons_commands"
9
9
  require_relative "commands/domains_commands"
10
+ require_relative "commands/addons_commands"
10
11
  require_relative "commands/db_commands"
11
12
  require_relative "commands/user_login"
12
13
  require_relative "commands/auth_commands"
@@ -66,6 +67,9 @@ module Captive
66
67
  desc "domains SUBCOMMAND", "Domaines d'une app"
67
68
  subcommand "domains", Commands::DomainsCommands
68
69
 
70
+ desc "addons SUBCOMMAND", "Add-ons d'une app (bases, Supabase…)"
71
+ subcommand "addons", Commands::AddonsCommands
72
+
69
73
  desc "auth SUBCOMMAND", "Jeton pour kubectl", hide: true
70
74
  subcommand "auth", Commands::AuthCommands
71
75
 
@@ -146,13 +150,12 @@ module Captive
146
150
 
147
151
  desc "logs APP", "Affiche les logs de l'app"
148
152
  long_desc <<~DESC
149
- MODE TEMPS RÉEL (défaut) — stream les logs kubectl du pod running.
153
+ MODE TEMPS RÉEL (défaut) — dernières lignes du web et des one-offs, via le dashboard.
150
154
  -f, --follow Suit en continu (Ctrl+C pour arrêter)
151
155
  -n, --lines N Nombre de lignes affichées (défaut : 100)
152
- -c, --container NOM Container spécifique (ex : migrate, redis)
153
- -w, --worker Cible le pod worker au lieu du pod web
156
+ -w, --worker Affiche le worker au lieu du web
154
157
  --raw Garde les lignes rack-timeout de la readinessProbe
155
- En cas de CrashLoopBackOff, --previous est activé automatiquement.
158
+ Il suffit d'être Membre de l'Organization de l'app, sans accès au repo GitHub.
156
159
 
157
160
  MODE HISTORIQUE (--from / --to) — interroge Loki via le dashboard.
158
161
  --from DATE Début de la plage (requis pour ce mode)
@@ -165,14 +168,12 @@ module Captive
165
168
  \x05 captive logs my-app
166
169
  \x05 captive logs my-app -f
167
170
  \x05 captive logs my-app -n 200
168
- \x05 captive logs my-app -c migrate
169
171
  \x05 captive logs my-app --worker
170
172
  \x05 captive logs my-app --raw
171
173
  \x05 captive logs my-app --from "2026-08-31 23:00" --to "2026-09-01 00:00"
172
174
  \x05 captive logs my-app --from "2026-08-31 23:00"
173
175
  DESC
174
176
  method_option :follow, aliases: "-f", type: :boolean, default: false
175
- method_option :container, aliases: "-c", type: :string
176
177
  method_option :worker, aliases: "-w", type: :boolean, default: false
177
178
  method_option :lines, aliases: "-n", type: :numeric, default: 100
178
179
  method_option :raw, type: :boolean, default: false, desc: "Affiche le bruit rack-timeout des sondes readiness"
@@ -181,9 +182,8 @@ module Captive
181
182
  def logs(app)
182
183
  from = parse_time(options[:from])
183
184
  to = parse_time(options[:to])
184
- Commands::Logs.new(kubectl: kubectl).run(
185
- app, follow: options[:follow], container: options[:container],
186
- worker: options[:worker], lines: options[:lines],
185
+ Commands::Logs.new.run(
186
+ app, follow: options[:follow], worker: options[:worker], lines: options[:lines],
187
187
  from: from, to: to, raw: options[:raw]
188
188
  )
189
189
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captive
4
+ module Commands
5
+ # Les Add-ons d'une app via le dashboard (ADR 0034) : tout ce que la page Add-ons permet,
6
+ # sans quitter le terminal. L'ajout est réservé aux admins de l'organisation.
7
+ class AddonsCommands < Thor
8
+ include CLIDependencies
9
+
10
+ def self.exit_on_failure? = true
11
+
12
+ desc "list APP", "Liste les add-ons de l'app et leur statut"
13
+ long_desc <<~DESC
14
+ \x05Exemple :
15
+ \x05 captive addons list my-app-staging
16
+ DESC
17
+ def list(app)
18
+ installed = addons_api.list(app)["installed"]
19
+ return puts(paint.dim("Aucun add-on. Voir : captive addons catalog #{app}")) if installed.empty?
20
+
21
+ installed.each { |addon| puts presenter.installed(addon) }
22
+ end
23
+
24
+ desc "catalog APP", "Liste les add-ons qu'on peut ajouter à l'app"
25
+ long_desc <<~DESC
26
+ \x05Exemple :
27
+ \x05 captive addons catalog my-app-staging
28
+ DESC
29
+ def catalog(app)
30
+ addons_api.list(app)["catalog"].each { |entry| puts presenter.catalog(entry) }
31
+ end
32
+
33
+ desc "add APP ADDON", "Ajoute un add-on à l'app (postgres, supabase…)"
34
+ long_desc <<~DESC
35
+ Un add-on « sur demande » (redis, egress-ip) prévient l'équipe plateforme au lieu de
36
+ l'ajouter. Réservé aux admins de l'organisation.
37
+
38
+ \x05Exemple :
39
+ \x05 captive addons add my-app-staging supabase
40
+ DESC
41
+ def add(app, addon)
42
+ result = addons_api.add(app, addon)
43
+ if result["status"] == "requested"
44
+ puts paint.green(" ✓ #{addon} demandé à l'équipe plateforme")
45
+ else
46
+ puts paint.green(" ✓ #{addon} en cours d'ajout : #{ASYNC_NOTICE}")
47
+ puts paint.dim(" Suivre : captive addons list #{app}")
48
+ end
49
+ end
50
+
51
+ desc "auth APP", "Affiche les réglages d'authentification de l'add-on Supabase"
52
+ long_desc <<~DESC
53
+ Auto-confirmation des emails et inscriptions ouvertes, en lecture seule : seule
54
+ l'équipe plateforme les modifie.
55
+
56
+ \x05Exemple :
57
+ \x05 captive addons auth my-app-staging
58
+ DESC
59
+ def auth(app)
60
+ Presenter::SupabaseAuth.new(paint: paint).lines(addons_api.supabase_auth(app), app: app).each { |line| puts line }
61
+ end
62
+
63
+ private
64
+
65
+ def presenter = Presenter::Addon.new(paint: paint)
66
+ end
67
+ end
68
+ end
@@ -2,36 +2,47 @@
2
2
 
3
3
  require "time"
4
4
  require_relative "../log_noise_filter"
5
+ require_relative "../log_stream"
5
6
  require_relative "../null_log_filter"
6
- require_relative "../one_off_spec"
7
7
  require_relative "../presenter/log_colorizer"
8
8
 
9
9
  module Captive
10
10
  module Commands
11
- # Affiche les logs d'une app en mode live (kubectl) ou historique (Loki via dashboard).
11
+ # Affiche les logs d'une app, toujours via le dashboard (Loki) : seule l'Organization décide
12
+ # de l'accès, plus le repo GitHub. Dernières lignes, suivi en direct (-f) ou plage (--from/--to).
12
13
  class Logs
13
- def initialize(kubectl:, dashboard: DashboardClient.new, paint: Pastel.new)
14
- @kubectl = kubectl
14
+ WORKER = "worker"
15
+
16
+ def initialize(dashboard: DashboardClient.new, stream: LogStream.new, paint: Pastel.new)
15
17
  @dashboard = dashboard
18
+ @stream = stream
16
19
  @colorizer = Presenter::LogColorizer.new(paint: paint)
17
20
  end
18
21
 
19
- def run(app, follow: false, container: nil, worker: false, lines: 100, from: nil, to: nil, raw: false)
22
+ def run(app, follow: false, worker: false, lines: 100, from: nil, to: nil, raw: false)
20
23
  if from || to
21
- run_historical(app, from: from, to: to, lines: lines, raw: raw)
24
+ run_historical(app, from: from, to: to, raw: raw)
22
25
  else
23
- run_live(app, follow: follow, container: container, worker: worker, lines: lines, raw: raw)
26
+ run_live(app, follow: follow, worker: worker, lines: lines, raw: raw)
24
27
  end
25
28
  end
26
29
 
27
30
  private
28
31
 
29
- def run_live(app, follow:, container:, worker:, lines:, raw:)
30
- selector = worker ? "app=#{app}-worker" : OneOffSpec.live_logs_selector(app)
31
- crashing = !follow && @kubectl.pod_crashing?(app, selector)
32
- warn "⚠️ Pod en crash — affichage des logs du dernier run (--previous)" if crashing
33
- @kubectl.logs(app, selector, follow: follow, container: container, previous: crashing, tail: lines,
34
- filter: log_filter(raw))
32
+ def run_live(app, follow:, worker:, lines:, raw:)
33
+ printer = live_printer(worker, log_filter(raw))
34
+ follow ? @stream.call(app, tail: lines, &printer) : @dashboard.logs(app, limit: lines).each(&printer)
35
+ rescue Interrupt
36
+ nil
37
+ end
38
+
39
+ # Comme le sélecteur kubectl d'avant : le web et les one-offs, ou le worker seul.
40
+ def live_printer(worker, filter)
41
+ lambda do |line|
42
+ next if (line["process"] == WORKER) != worker || filter.noise?(line["text"].to_s)
43
+
44
+ $stdout.puts format_line(line)
45
+ end
35
46
  end
36
47
 
37
48
  def run_historical(app, from:, to:, raw:, **)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captive
4
+ module Dashboard
5
+ # Les Add-ons d'une app et le Catalogue (ADR 0034) : la même API que la page Add-ons du
6
+ # dashboard. Un ajout est appliqué plus tard par captive-platform (réponse 202).
7
+ class AddonsResource
8
+ PATH = AppPath.new("addons")
9
+ SUPABASE_AUTH_PATH = AppPath.new("addons/supabase/auth")
10
+
11
+ def initialize(api:)
12
+ @api = api
13
+ end
14
+
15
+ def list(app) = @api.get(PATH.call(app))
16
+ def add(app, addon) = @api.post(PATH.call(app), { addon: addon })
17
+
18
+ # En lecture seule : seule l'équipe plateforme modifie ces réglages, dans le dépôt GitOps.
19
+ def supabase_auth(app) = @api.get(SUPABASE_AUTH_PATH.call(app))
20
+ end
21
+ end
22
+ end
@@ -4,9 +4,7 @@ require "base64"
4
4
  require "json"
5
5
  require "open3"
6
6
  require_relative "errors"
7
- require_relative "log_noise_filter"
8
7
  require_relative "one_off_spec"
9
- require_relative "presenter/log_line"
10
8
 
11
9
  module Captive
12
10
  class Kubectl
@@ -174,42 +172,6 @@ module Captive
174
172
  system("kubectl", *kubeconfig_args, "exec", "-n", namespace, "-i", pod, "--", *cmd, in: File.open(file))
175
173
  end
176
174
 
177
- def pod_crashing?(namespace, selector)
178
- json = run("get", "pod", "-n", namespace, "-l", selector, "-o", "json")
179
- items = JSON.parse(json)["items"] || []
180
- items.any? do |pod|
181
- statuses = (pod.dig("status", "containerStatuses") || []) +
182
- (pod.dig("status", "initContainerStatuses") || [])
183
- statuses.any? do |s|
184
- s["restartCount"].to_i > 0 ||
185
- s.dig("state", "waiting", "reason")&.include?("CrashLoop") ||
186
- s.dig("lastState", "terminated", "exitCode").to_i != 0
187
- end
188
- end
189
- end
190
-
191
- def logs(namespace, selector, follow: false, container: nil, previous: false, tail: 100,
192
- formatter: Presenter::LogLine.new(app: namespace), filter: LogNoiseFilter.new)
193
- args = [ "logs", "-n", namespace, "-l", selector, "--timestamps", "--prefix" ]
194
- args << "-f" if follow
195
- args << "-p" if previous
196
- args += [ "--tail", tail.to_s ] unless follow
197
- args += [ "-c", container ] if container
198
- success = print_logs(args, formatter, filter)
199
- return if success || !previous
200
-
201
- warn "⚠️ Logs du run précédent non disponibles (purgés par le runtime), affichage des logs actuels"
202
- print_logs(args - [ "-p" ], formatter, filter)
203
- end
204
-
205
- def print_logs(args, formatter, filter)
206
- stream_logs(*args) do |line|
207
- next if filter.noise?(line)
208
-
209
- puts formatter.call(line)
210
- end
211
- end
212
-
213
175
  def run_interactive(*args)
214
176
  system("kubectl", *kubeconfig_args, *args)
215
177
  end
@@ -18,9 +18,12 @@ require_relative "dashboard/apps_resource"
18
18
  require_relative "dashboard/env_vars_resource"
19
19
  require_relative "dashboard/scheduled_tasks_resource"
20
20
  require_relative "dashboard/domains_resource"
21
+ require_relative "dashboard/addons_resource"
21
22
  require_relative "presenter/app_info"
22
23
  require_relative "presenter/scheduled_task"
23
24
  require_relative "presenter/domain"
25
+ require_relative "presenter/addon"
26
+ require_relative "presenter/supabase_auth"
24
27
  require_relative "activity_trail"
25
28
  require_relative "errors"
26
29
  require_relative "ui/spinner"
@@ -35,6 +38,3 @@ require_relative "commands/domain_wait"
35
38
  require_relative "commands/domain_announcer"
36
39
  require_relative "commands/db_dump"
37
40
  require_relative "commands/db_restore"
38
-
39
-
40
- require_relative "presenter/log_line"
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require_relative "dashboard_client"
7
+ require_relative "dashboard_response"
8
+ require_relative "session_token"
9
+ require_relative "sse_parser"
10
+
11
+ module Captive
12
+ # Suit les logs d'une app en direct, en Server-Sent Events depuis le dashboard
13
+ # (`GET /api/apps/:id/logs/stream`). Le dashboard ferme le flux au bout de quelques minutes :
14
+ # on se reconnecte alors avec le dernier id reçu, l'horodatage de la dernière ligne, pour
15
+ # reprendre sans perte ni doublon. Seul Ctrl+C (Interrupt) ou un refus du dashboard l'arrête.
16
+ class LogStream
17
+ RECONNECT_DELAY = 1
18
+ NETWORK_ERRORS = [ IOError, SystemCallError, Net::OpenTimeout, Net::ReadTimeout, OpenSSL::SSL::SSLError ].freeze
19
+
20
+ def initialize(session: SessionToken.new)
21
+ @session = session
22
+ end
23
+
24
+ def self.stream_uri(app, tail)
25
+ uri = URI.parse("#{DashboardClient::DASHBOARD_URL}/api/apps/#{URI.encode_uri_component(app)}/logs/stream")
26
+ uri.query = URI.encode_www_form(tail:)
27
+ uri
28
+ end
29
+
30
+ def self.headers(last_id)
31
+ last_id ? { "Accept" => "text/event-stream", "Last-Event-ID" => last_id } : { "Accept" => "text/event-stream" }
32
+ end
33
+
34
+ def self.read(response, &)
35
+ DashboardResponse.new(response).parse unless response.code.to_i == 200
36
+ response.read_body(&)
37
+ end
38
+
39
+ def call(app, tail:, &)
40
+ uri = self.class.stream_uri(app, tail)
41
+ @last_id = nil
42
+ loop do
43
+ follow(uri, &)
44
+ rescue *NETWORK_ERRORS
45
+ sleep RECONNECT_DELAY
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ # @last_id avance à chaque événement, pas en fin de connexion : une coupure réseau ne doit
52
+ # pas faire reprendre plus tôt, ce qui réafficherait des lignes.
53
+ def follow(uri, &)
54
+ parser = SseParser.new
55
+ open_stream(uri, self.class.headers(@last_id)) { |chunk| relay(parser, chunk, &) }
56
+ end
57
+
58
+ def relay(parser, chunk)
59
+ parser.feed(chunk) do |event|
60
+ @last_id = event[:id] || @last_id
61
+ yield JSON.parse(event[:data])
62
+ end
63
+ end
64
+
65
+ # Pas de read_timeout court : le flux reste ouvert, le serveur envoie un battement de cœur.
66
+ def open_stream(uri, headers, &)
67
+ @session.authorize do |token|
68
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 5, read_timeout: 60) do |http|
69
+ request = Net::HTTP::Get.new(uri, headers.merge("Authorization" => "Bearer #{token}"))
70
+ http.request(request) { |response| self.class.read(response, &) }
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -45,10 +45,6 @@ module Captive
45
45
  "#{app}-run"
46
46
  end
47
47
 
48
- def self.live_logs_selector(app)
49
- "app in (#{app},#{run_app_label(app)})"
50
- end
51
-
52
48
  private
53
49
 
54
50
  def job_manifest
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captive
4
+ module Presenter
5
+ # Une ligne par Add-on présent (« Les vôtres ») ou proposé (« Catalogue »).
6
+ class Addon
7
+ def initialize(paint:)
8
+ @paint = paint
9
+ end
10
+
11
+ def installed(addon)
12
+ details = [ addon["name"], addon["tier"]&.upcase ].compact.join(" ")
13
+ [ @paint.bold(addon["addon"]), details, status(addon["status"]) ].reject(&:empty?).join(" ")
14
+ end
15
+
16
+ def catalog(entry)
17
+ head = "#{@paint.bold(entry["id"])} #{entry["description"]}"
18
+ notes = []
19
+ notes << "inclut #{entry["includes"].join(", ")}" if entry["includes"].any?
20
+ notes << "sur demande" if entry["mode"] == "request"
21
+ notes << @paint.green("ajouté") if entry["installed"]
22
+ notes.empty? ? head : "#{head} #{@paint.dim("(#{notes.join(", ")})")}"
23
+ end
24
+
25
+ private
26
+
27
+ def status(value) = value == "ready" ? @paint.green("✓") : @paint.yellow("en cours")
28
+ end
29
+ end
30
+ end
@@ -26,6 +26,8 @@ module Captive
26
26
  end
27
27
 
28
28
  def workload(entry)
29
+ return "#{entry["name"]} #{@paint.yellow(entry["status_reason"])}" if entry["status"] == "config_required"
30
+
29
31
  ready = "#{entry["ready"]}/#{entry["total"]}"
30
32
  ready = @paint.yellow(ready) unless entry["ready"] == entry["total"]
31
33
  "#{entry["name"]} #{ready} prêt(s), tier #{entry["tier"] || "–"}"
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captive
4
+ module Presenter
5
+ # Les réglages d'authentification de l'Add-on Supabase, en lecture seule comme sur la page
6
+ # du dashboard : une valeur que le dashboard n'a pas pu lire reste « inconnu ».
7
+ class SupabaseAuth
8
+ VALEURS = { true => "oui", false => "non" }.freeze
9
+
10
+ def initialize(paint:)
11
+ @paint = paint
12
+ end
13
+
14
+ def lines(settings, app:)
15
+ [
16
+ [ "Auto-confirmation des emails", valeur(settings["autoconfirm"]), alerte(settings, app) ].compact.join(" "),
17
+ "Inscriptions ouvertes #{valeur(settings["signup_open"])}",
18
+ @paint.dim("Modifiable par l'équipe plateforme"),
19
+ ]
20
+ end
21
+
22
+ private
23
+
24
+ def valeur(value) = VALEURS.fetch(value, "inconnu")
25
+
26
+ def alerte(settings, app)
27
+ @paint.red("⚠ active en production") if settings["autoconfirm"] && app.end_with?("-production")
28
+ end
29
+ end
30
+ end
31
+ end
@@ -2,13 +2,14 @@
2
2
 
3
3
  module Captive
4
4
  module ShellCompletions
5
- ROOT_COMMANDS = %w[apps env crons domains db login whoami console run logs version completions].freeze
5
+ ROOT_COMMANDS = %w[apps env crons domains addons db login whoami console run logs version completions].freeze
6
6
 
7
7
  SUBCOMMANDS = {
8
8
  "apps" => %w[list info restart rebuild],
9
9
  "env" => %w[list set unset],
10
10
  "crons" => %w[list add edit rm suspend resume trigger runs],
11
11
  "domains" => %w[list add rm redirect serve],
12
+ "addons" => %w[list catalog add],
12
13
  "db" => %w[restore],
13
14
  }.freeze
14
15
 
@@ -25,6 +26,7 @@ module Captive
25
26
  env) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["env"].join(" ")}" -- "$cur")); return ;;
26
27
  crons) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["crons"].join(" ")}" -- "$cur")); return ;;
27
28
  domains) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["domains"].join(" ")}" -- "$cur")); return ;;
29
+ addons) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["addons"].join(" ")}" -- "$cur")); return ;;
28
30
  db) COMPREPLY=($(compgen -W "#{SUBCOMMANDS["db"].join(" ")}" -- "$cur")); return ;;
29
31
  esac
30
32
 
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captive
4
+ # Découpe un flux Server-Sent Events, reçu par morceaux arbitraires, en événements
5
+ # `{ id:, data: }`. Les commentaires (`:`, battements de cœur) et `retry` sont ignorés.
6
+ class SseParser
7
+ def initialize
8
+ @buffer = +""
9
+ end
10
+
11
+ def feed(chunk)
12
+ @buffer << chunk
13
+ while (fin = @buffer.index("\n\n"))
14
+ event = self.class.parse(@buffer.slice!(0, fin + 2))
15
+ yield event if event[:data]
16
+ end
17
+ end
18
+
19
+ def self.parse(bloc)
20
+ bloc.each_line(chomp: true).each_with_object({}) do |ligne, event|
21
+ champ, valeur = ligne.split(": ", 2)
22
+ event[champ.to_sym] = valeur if %w[id data].include?(champ)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Captive
4
- VERSION = "1.4.0"
4
+ VERSION = "1.6.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: captive-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Captive Studio
@@ -107,6 +107,7 @@ files:
107
107
  - lib/captive/cli.rb
108
108
  - lib/captive/cli/dependencies.rb
109
109
  - lib/captive/cluster_endpoint.rb
110
+ - lib/captive/commands/addons_commands.rb
110
111
  - lib/captive/commands/apps_commands.rb
111
112
  - lib/captive/commands/apps_restart.rb
112
113
  - lib/captive/commands/auth_commands.rb
@@ -131,6 +132,7 @@ files:
131
132
  - lib/captive/commands/user_login.rb
132
133
  - lib/captive/credentials.rb
133
134
  - lib/captive/credentials_store.rb
135
+ - lib/captive/dashboard/addons_resource.rb
134
136
  - lib/captive/dashboard/app_path.rb
135
137
  - lib/captive/dashboard/apps_resource.rb
136
138
  - lib/captive/dashboard/domains_resource.rb
@@ -146,18 +148,21 @@ files:
146
148
  - lib/captive/kubectl.rb
147
149
  - lib/captive/loader.rb
148
150
  - lib/captive/log_noise_filter.rb
151
+ - lib/captive/log_stream.rb
149
152
  - lib/captive/null_log_filter.rb
150
153
  - lib/captive/one_off_spec.rb
151
154
  - lib/captive/poller.rb
155
+ - lib/captive/presenter/addon.rb
152
156
  - lib/captive/presenter/ansi_stripper.rb
153
157
  - lib/captive/presenter/app_info.rb
154
158
  - lib/captive/presenter/domain.rb
155
159
  - lib/captive/presenter/log_colorizer.rb
156
- - lib/captive/presenter/log_line.rb
157
160
  - lib/captive/presenter/scheduled_task.rb
161
+ - lib/captive/presenter/supabase_auth.rb
158
162
  - lib/captive/session_id.rb
159
163
  - lib/captive/session_token.rb
160
164
  - lib/captive/shell_completions.rb
165
+ - lib/captive/sse_parser.rb
161
166
  - lib/captive/terminal.rb
162
167
  - lib/captive/token_refresher.rb
163
168
  - lib/captive/ui/banner.rb
@@ -1,40 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "captive/log_utils"
4
- require_relative "log_colorizer"
5
-
6
- module Captive
7
- module Presenter
8
- # Parses a raw kubectl log line and renders it as a colorized terminal string.
9
- class LogLine
10
- BRACKET_RE = /\A\[(?:pod\/)?(?<pod>[^\/\]]+)(?:\/(?<container>[^\/\]]+))?\]\z/
11
-
12
- def initialize(app:, paint: Pastel.new)
13
- @app = app
14
- @colorizer = LogColorizer.new(paint: paint)
15
- end
16
-
17
- def call(raw_line)
18
- line = raw_line.chomp
19
- first, second, message = line.split(" ", 3)
20
- return line if first.nil? || second.nil? || message.nil?
21
-
22
- bracket, ts = pick_bracket_and_timestamp(first, second)
23
- return line unless bracket
24
-
25
- label = LogUtils::ProcessLabel.for(pod: bracket[:pod], namespace: @app, container: bracket[:container])
26
- @colorizer.format_line(ts: ts, label: label, message: message)
27
- end
28
-
29
- private
30
-
31
- def pick_bracket_and_timestamp(first, second)
32
- if (bracket = BRACKET_RE.match(first))
33
- [ bracket, second ]
34
- elsif (bracket = BRACKET_RE.match(second))
35
- [ bracket, first ]
36
- end
37
- end
38
- end
39
- end
40
- end