captive-stack-detector 0.9.2 → 0.12.4

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: 390061bda035fdc2b686a8fa5b9cffd1df43bfec160ea01147444f7f07d2526d
4
- data.tar.gz: 71c351bbf691e952d2aa5972e2a9c22d27caf5fdca2da4bcffe07188a8293e56
3
+ metadata.gz: b90c2a1855bd23b857f4f142a0c674795992d33a98512e4b7a1cbcfb7db5df7f
4
+ data.tar.gz: 49a6d7ae837f68ed2e0995fcb8623249ceb93168ddac120108c5b032c93afff5
5
5
  SHA512:
6
- metadata.gz: fe04ff00ac23c390404eb2176fded572115311cf81f023f04c6cf2c4d0bfe8273186491151e73e1e252f7f57e7601c24f94949aae55edb3fcedcbd03991e8f00
7
- data.tar.gz: '08fa5dd4dc07f8b6026b581a3259ba07dbbd66cad39ec08696235dc48fe28f800ddd87c3505782e5db7ba9eeb0f46e441acf91d08c87a0945208a61c4d48d836'
6
+ metadata.gz: d430ba6a7b0077093dff4a48d756b106e5f47c64f8fe981d7fbf18c5c0c89035ff8dd045e6b2da56d9705f607ab0d0a626e67247f72aa51771fb4c1b5b972c1f
7
+ data.tar.gz: a89e85828b1be7b804c4ef860ddc7664a3354e8c78c719714c3cd16229a0eca4d85f1084d3be6248d16a5f706d8e87d7b0a83642c2551390c90d04db75ad50e9
data/docs/stacks.md CHANGED
@@ -112,13 +112,26 @@ Ces variables sont lues **au build** : après en avoir changé une, redéployez.
112
112
 
113
113
  ### Site statique sans build
114
114
 
115
- Condition : ni `Gemfile` ni `package.json` à la racine, mais un `public/index.html` ou un `index.html` à la racine. Rien n'est installé ni buildé : vos fichiers sont servis tels quels.
115
+ Condition : pas de `Gemfile` à la racine, mais un `public/index.html` ou un `index.html` à la racine. Un `package.json` est accepté s'il ne sert qu'à l'outillage (Supabase CLI, prettier…) : sans dépendance `expo` ni script `start` ou `build`. Rien n'est installé ni buildé : vos fichiers sont servis tels quels.
116
116
 
117
117
  - Si `public/` existe, seul ce dossier est servi.
118
118
  - Sinon la racine est servie, limitée aux fichiers web (HTML, JavaScript, CSS, JSON, images, polices, manifeste, texte). Les dossiers cachés (`.git/`, `.github/`), `supabase/`, vos scripts SQL ou votre documentation ne sont jamais publiés.
119
119
  - Un dossier `supabase/` avec son `config.toml` provisionne un backend Supabase et sa base Postgres.
120
120
  - Faute de build, aucune variable n'est injectée : votre code lit directement l'adresse de l'API, par exemple `location.origin`.
121
121
 
122
+ ## Options Supabase recommandées
123
+
124
+ Avec un backend Supabase (application Node ou site statique), Captive lit votre code et vous recommande les options de l'add-on dont il a besoin, chacune avec sa raison (par exemple « votre code appelle signUp »). Une recommandation ne s'active pas seule : vous la retrouvez dans le dashboard au moment de la création.
125
+
126
+ | Option | Recommandée quand |
127
+ | --- | --- |
128
+ | `smtp` | votre JavaScript ou TypeScript appelle `signUp`, `resetPasswordForEmail`, `signInWithOtp`, `inviteUserByEmail` ou `updateUser({ email })` : Supabase doit envoyer un email. Une connexion par `signInWithPassword` ou la double authentification (`auth.mfa`) n'en envoie pas. |
129
+ | `realtime` | votre code s'abonne à un canal (`.channel(`) ou aux changements de la base (`postgres_changes`). |
130
+ | `pg_cron` | un fichier `.sql` du dépôt (dont `supabase/migrations/` et `db/`) appelle `cron.schedule`, ou `net.http_` (pg_net est livré avec pg_cron). |
131
+ | `vault` | un fichier `.sql` utilise le coffre Supabase (`vault.create_secret`, `vault.decrypted_secrets`…). |
132
+
133
+ Les dépendances (`node_modules/`), sorties de build, tests et fichiers vendored sont ignorés, comme pour les variables. L'analyse est statique : un appel construit dynamiquement n'est pas vu ; activez alors l'option vous-même.
134
+
122
135
  ## Expo
123
136
 
124
137
  Condition : un `package.json` à la racine avec la dépendance `expo` (dans `dependencies` ou `devDependencies`). Aucun script n'est exigé.
@@ -137,6 +150,6 @@ Condition : un `package.json` à la racine avec la dépendance `expo` (dans `dep
137
150
  | --- | --- | --- |
138
151
  | Aucun Gemfile ni package.json à la racine du dépôt. | Pas de fichier de dépendances ni d'`index.html` à la racine. | Vérifiez que l'application est bien à la racine du dépôt, pas dans un sous-dossier (`app/`, `frontend/`…). Les monorepos ne sont pas pris en charge. |
139
152
  | Gemfile trouvé, mais sans la gem rails. | Application Ruby non Rails (Sinatra, Hanami…), ou `Gemfile` d'outillage dans un projet JavaScript. | Seul Rails est pris en charge côté Ruby. Si le `Gemfile` ne sert qu'à de l'outillage, retirez-le du dépôt. |
140
- | package.json trouvé, mais sans dépendance expo ni script start ou build. | Captive ne sait pas comment construire l'application. | Ajoutez un script `build` (application web) ou `start` (serveur), comme dans les exemples ci-dessus. |
153
+ | package.json trouvé, mais sans dépendance expo ni script start ou build. | Captive ne sait pas comment construire l'application, et aucun `index.html` ne permet de la servir telle quelle. | Ajoutez un script `build` (application web) ou `start` (serveur), comme dans les exemples ci-dessus, ou un `index.html` pour un site statique. |
141
154
 
142
155
  Si votre stack n'est pas listée ici, contactez l'équipe Captive.
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "recommendation_rule"
4
+ require_relative "repo_sources"
5
+
6
+ module CaptiveStackDetector
7
+ # Propose les options de l'add-on Supabase que le code du dépôt utilise, chacune avec sa raison.
8
+ class AddonRecommender
9
+ # Appels d'auth qui envoient un email ; signInWithPassword et auth.mfa n'en envoient pas.
10
+ AUTH_EMAILS = /
11
+ \b(?<call>signUp|resetPasswordForEmail|signInWithOtp|inviteUserByEmail)\(
12
+ | \b(?<call>updateUser)\(\s*\{\s*email\b
13
+ /x
14
+ RULES = [
15
+ RecommendationRule.new("smtp", :js, AUTH_EMAILS),
16
+ RecommendationRule.new("realtime", :js, /\.(?<call>channel)\(|(?<call>postgres_changes)/),
17
+ RecommendationRule.new("pg_cron", :sql, /\b(?<call>cron\.schedule|net\.http_)/),
18
+ RecommendationRule.new("vault", :sql, /\b(?<call>vault)\./),
19
+ ].freeze
20
+
21
+ def initialize(reader)
22
+ @reader = reader
23
+ end
24
+
25
+ # Seul Supabase a des options : les autres stacks gardent recommendations vide, sans lire les sources.
26
+ def apply(result)
27
+ return result unless result.services.backend == "supabase"
28
+
29
+ sources = RepoSources.new(@reader)
30
+ result.with(recommendations: RULES.filter_map { |rule| rule.recommend(sources) })
31
+ end
32
+ end
33
+ end
@@ -11,6 +11,8 @@ module CaptiveStackDetector
11
11
  @analyzer = PackageJsonAnalyzer.new(package_json)
12
12
  end
13
13
 
14
+ def entry? = !@analyzer.type.nil?
15
+
14
16
  def detect
15
17
  type = @analyzer.type
16
18
  raise UnsupportedStack, :no_node_entry unless type
@@ -20,7 +20,7 @@ module CaptiveStackDetector
20
20
  subtype: @analyzer.subtype,
21
21
  services: Services.new(database: @analyzer.database, queue: @analyzer.queue, backend: nil),
22
22
  worker: build_worker,
23
- runtime: Runtime.new(ruby: @reader.ruby_version, node: nil),
23
+ runtime: Runtime.new(ruby: @reader.ruby_version, node: @reader.node_version),
24
24
  env_vars: @reader.env_vars,
25
25
  system_packages: system_packages,
26
26
  )
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "types"
4
+
5
+ module CaptiveStackDetector
6
+ # Une option d'add-on et les appels qui la justifient ; le groupe nommé call est l'appel cité dans la raison.
7
+ class RecommendationRule < Data.define(:option, :kind, :pattern)
8
+ LABELS = { js: "votre code", sql: "votre SQL" }.freeze
9
+
10
+ def recommend(sources)
11
+ call = pattern.match(sources.of(kind))&.[](:call)
12
+ call && Recommendation.new(addon: "supabase", option: option, reason: "#{LABELS.fetch(kind)} appelle #{call}")
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "js_env_files"
4
+
5
+ module CaptiveStackDetector
6
+ # Contenu des sources JS et SQL du dépôt, hors dépendances, builds et tests (mêmes exclusions que JsEnvFiles).
7
+ class RepoSources
8
+ def self.kind(path)
9
+ return nil if JsEnvFiles::EXCLUDED.match?(path)
10
+ return :sql if path.end_with?(".sql")
11
+
12
+ :js if JsEnvFiles::SOURCE.match?(path)
13
+ end
14
+
15
+ def initialize(reader)
16
+ @reader = reader
17
+ @paths = reader.list(".").sort.map { |entry| entry.delete_prefix("./") }.group_by { |path| self.class.kind(path) }
18
+ end
19
+
20
+ # Tout le code d'un genre (:js ou :sql), fichier par fichier, en un seul texte.
21
+ def of(kind) = @paths.fetch(kind, []).map { |path| @reader.read(path).to_s }.join("\n")
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "types"
4
+ require_relative "gemfile_analyzer"
5
+ require_relative "rails_stack_detector"
6
+ require_relative "js_stack_detector"
7
+ require_relative "static_site_detector"
8
+
9
+ module CaptiveStackDetector
10
+ # Aiguille un dépôt vers le détecteur de sa stack, d'après ses manifestes à la racine.
11
+ class StackRouter
12
+ def initialize(reader)
13
+ @reader = reader
14
+ end
15
+
16
+ def detect
17
+ gemfile = @reader.read("Gemfile")
18
+ package_json = @reader.read("package.json")
19
+
20
+ return RailsStackDetector.new(@reader, GemfileAnalyzer.new(gemfile)).detect if gemfile
21
+
22
+ static_site = StaticSiteDetector.new(@reader)
23
+ # Un package.json sans point d'entrée (Supabase CLI, prettier…) n'est que de l'outillage :
24
+ # un index.html présent reste servi tel quel au lieu d'un refus no_node_entry.
25
+ js = package_json && JsStackDetector.new(@reader, package_json)
26
+ return js.detect if js && (js.entry? || !static_site.site?)
27
+ raise UnsupportedStack, :no_manifest unless static_site.site?
28
+
29
+ static_site.detect
30
+ end
31
+ end
32
+ end
@@ -4,12 +4,14 @@ module CaptiveStackDetector
4
4
  Services = Data.define(:database, :queue, :backend)
5
5
  Worker = Data.define(:command)
6
6
  Runtime = Data.define(:ruby, :node)
7
+ # Option d'add-on que la détection propose d'activer, avec sa raison lisible par le client.
8
+ Recommendation = Data.define(:addon, :option, :reason)
7
9
  # public_env_prefix : préfixe des variables que le bundler expose au navigateur
8
10
  # (VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_), nil sans bundler front (PackageJsonAnalyzer).
9
11
  Result = Data.define(:type, :subtype, :services, :worker, :runtime, :env_vars, :system_packages,
10
- :public_env_prefix) do
12
+ :public_env_prefix, :recommendations) do
11
13
  # env_vars : { "NOM" => valeur }, fusionné par captive-admin au provisionnement.
12
- def initialize(env_vars:, public_env_prefix: nil, **)
14
+ def initialize(env_vars:, public_env_prefix: nil, recommendations: [], **)
13
15
  Hash.try_convert(env_vars) or raise ArgumentError, "env_vars doit être un Hash"
14
16
  super
15
17
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CaptiveStackDetector
4
- VERSION = "0.9.2"
4
+ VERSION = "0.12.4"
5
5
  end
@@ -3,11 +3,10 @@
3
3
  require "json"
4
4
  require_relative "captive_stack_detector/types"
5
5
  require_relative "captive_stack_detector/file_reader"
6
- require_relative "captive_stack_detector/gemfile_analyzer"
7
- require_relative "captive_stack_detector/rails_stack_detector"
8
- require_relative "captive_stack_detector/js_stack_detector"
9
- require_relative "captive_stack_detector/static_site_detector"
6
+ require_relative "captive_stack_detector/stack_router"
7
+ require_relative "captive_stack_detector/addon_recommender"
10
8
 
9
+ # Déduit d'un dépôt sa stack, ses services et les options d'add-on à recommander.
11
10
  module CaptiveStackDetector
12
11
  # Doc publique des stacks supportées, livrée dans la gem et affichée par le dashboard (ADR 0003).
13
12
  DOC_PATH = File.expand_path("../docs/stacks.md", __dir__)
@@ -18,16 +17,7 @@ module CaptiveStackDetector
18
17
  end
19
18
 
20
19
  def self.detect_from(reader)
21
- gemfile = reader.read("Gemfile")
22
- package_json = reader.read("package.json")
23
-
24
- return RailsStackDetector.new(reader, GemfileAnalyzer.new(gemfile)).detect if gemfile
25
- return JsStackDetector.new(reader, package_json).detect if package_json
26
-
27
- static_site = StaticSiteDetector.new(reader)
28
- raise UnsupportedStack, :no_manifest unless static_site.site?
29
-
30
- static_site.detect
20
+ AddonRecommender.new(reader).apply(StackRouter.new(reader).detect)
31
21
  end
32
22
  private_class_method :detect_from
33
23
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: captive-stack-detector
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.2
4
+ version: 0.12.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Captive Studio
@@ -20,6 +20,7 @@ files:
20
20
  - docs/stacks.md
21
21
  - lib/captive/stack/detector.rb
22
22
  - lib/captive_stack_detector.rb
23
+ - lib/captive_stack_detector/addon_recommender.rb
23
24
  - lib/captive_stack_detector/aptfile_parser.rb
24
25
  - lib/captive_stack_detector/env_template_scanner.rb
25
26
  - lib/captive_stack_detector/env_vars_scanner.rb
@@ -38,11 +39,14 @@ files:
38
39
  - lib/captive_stack_detector/node_version_detector.rb
39
40
  - lib/captive_stack_detector/package_json_analyzer.rb
40
41
  - lib/captive_stack_detector/rails_stack_detector.rb
42
+ - lib/captive_stack_detector/recommendation_rule.rb
41
43
  - lib/captive_stack_detector/recurring_schedule.rb
44
+ - lib/captive_stack_detector/repo_sources.rb
42
45
  - lib/captive_stack_detector/ruby_version_detector.rb
43
46
  - lib/captive_stack_detector/solid_queue_usage.rb
44
47
  - lib/captive_stack_detector/solid_queue_worker.rb
45
48
  - lib/captive_stack_detector/ssr_framework_detector.rb
49
+ - lib/captive_stack_detector/stack_router.rb
46
50
  - lib/captive_stack_detector/static_site_detector.rb
47
51
  - lib/captive_stack_detector/system_package_detector.rb
48
52
  - lib/captive_stack_detector/types.rb