diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5f4625f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,656 @@ +# Tribes Sender Plugin Agent Guide + +This repository is the Tribes Sender plugin for RTMP ingest and HLS/LL-HLS +streaming. Keep changes scoped to sender-owned runtime, migration, asset, and +test surfaces. + +## Required Workflow + +- Use `scripts/plugin` for plugin-aware commands: `scripts/plugin validate`, + `scripts/plugin test`, `scripts/plugin precommit`, and `scripts/plugin smoke`. +- Use `devenv shell -- ` when running repo commands from outside the + devenv shell. +- Do not bypass the plugin wrapper for host-backed validation. + +## Plugin Contract + +- `manifest.json` `entry_module` must point at `Tribes.Plugins.Sender.Plugin`. +- Keep streaming supervision children restartable under normal OTP supervision. +- Keep runtime spec fields aligned with `manifest.json`. + + + + +## usage_rules usage +_A config-driven dev tool for Elixir projects to manage AGENTS.md files and agent skills from dependencies_ + +## Using Usage Rules + +Many packages have usage rules, which you should *thoroughly* consult before taking any +action. These usage rules contain guidelines and rules *directly from the package authors*. +They are your best source of knowledge for making decisions. + +## Modules & functions in the current app and dependencies + +When looking for docs for modules & functions that are dependencies of the current project, +or for Elixir itself, use `mix usage_rules.docs` + +``` +# Search a whole module +mix usage_rules.docs Enum + +# Search a specific function +mix usage_rules.docs Enum.zip + +# Search a specific function & arity +mix usage_rules.docs Enum.zip/1 +``` + + +## Searching Documentation + +You should also consult the documentation of any tools you are using, early and often. The best +way to accomplish this is to use the `usage_rules.search_docs` mix task. Once you have +found what you are looking for, use the links in the search results to get more detail. For example: + +``` +# Search docs for all packages in the current application, including Elixir +mix usage_rules.search_docs Enum.zip + +# Search docs for specific packages +mix usage_rules.search_docs Req.get -p req + +# Search docs for multi-word queries +mix usage_rules.search_docs "making requests" -p req + +# Search only in titles (useful for finding specific functions/modules) +mix usage_rules.search_docs "Enum.zip" --query-by title +``` + + + + +## usage_rules:elixir usage +# Elixir Core Usage Rules + +## Pattern Matching +- Use pattern matching over conditional logic when possible +- Prefer to match on function heads instead of using `if`/`else` or `case` in function bodies +- `%{}` matches ANY map, not just empty maps. Use `map_size(map) == 0` guard to check for truly empty maps + +## Error Handling +- Use `{:ok, result}` and `{:error, reason}` tuples for operations that can fail +- Avoid raising exceptions for control flow +- Use `with` for chaining operations that return `{:ok, _}` or `{:error, _}` + +## Common Mistakes to Avoid +- Elixir has no `return` statement, nor early returns. The last expression in a block is always returned. +- Don't use `Enum` functions on large collections when `Stream` is more appropriate +- Avoid nested `case` statements - refactor to a single `case`, `with` or separate functions +- Don't use `String.to_atom/1` on user input (memory leak risk) +- Lists and enumerables cannot be indexed with brackets. Use pattern matching or `Enum` functions +- Prefer `Enum` functions like `Enum.reduce` over recursion +- When recursion is necessary, prefer to use pattern matching in function heads for base case detection +- Using the process dictionary is typically a sign of unidiomatic code +- Only use macros if explicitly requested +- There are many useful standard library functions, prefer to use them where possible + +## Function Design +- Use guard clauses: `when is_binary(name) and byte_size(name) > 0` +- Prefer multiple function clauses over complex conditional logic +- Name functions descriptively: `calculate_total_price/2` not `calc/2` +- Predicate function names should not start with `is` and should end in a question mark. +- Names like `is_thing` should be reserved for guards + +## Data Structures +- Use structs over maps when the shape is known: `defstruct [:name, :age]` +- Prefer keyword lists for options: `[timeout: 5000, retries: 3]` +- Use maps for dynamic key-value data +- Prefer to prepend to lists `[new | list]` not `list ++ [new]` + +## Mix Tasks + +- Use `mix help` to list available mix tasks +- Use `mix help task_name` to get docs for an individual task +- Read the docs and options fully before using tasks + +## Testing +- Run tests in a specific file with `mix test test/my_test.exs` and a specific test with the line number `mix test path/to/test.exs:123` +- Limit the number of failed tests with `mix test --max-failures n` +- Use `@tag` to tag specific tests, and `mix test --only tag` to run only those tests +- Use `assert_raise` for testing expected exceptions: `assert_raise ArgumentError, fn -> invalid_function() end` +- Use `mix help test` to for full documentation on running tests + +## Debugging + +- Use `dbg/1` to print values while debugging. This will display the formatted value and other relevant information in the console. + + + +## usage_rules:otp usage +# OTP Usage Rules + +## GenServer Best Practices +- Keep state simple and serializable +- Handle all expected messages explicitly +- Use `handle_continue/2` for post-init work +- Implement proper cleanup in `terminate/2` when necessary + +## Process Communication +- Use `GenServer.call/3` for synchronous requests expecting replies +- Use `GenServer.cast/2` for fire-and-forget messages. +- When in doubt, use `call` over `cast`, to ensure back-pressure +- Set appropriate timeouts for `call/3` operations + +## Fault Tolerance +- Set up processes such that they can handle crashing and being restarted by supervisors +- Use `:max_restarts` and `:max_seconds` to prevent restart loops + +## Task and Async +- Use `Task.Supervisor` for better fault tolerance +- Handle task failures with `Task.yield/2` or `Task.shutdown/2` +- Set appropriate task timeouts +- Use `Task.async_stream/3` for concurrent enumeration with back-pressure + + + +## phoenix:ecto usage +## Ecto Guidelines + +- **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email` +- Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs` +- `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string` +- `Ecto.Changeset.validate_number/2` **DOES NOT SUPPORT the `:allow_nil` option**. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never needed +- You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields +- Fields which are set programmatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct +- **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied + + + +## phoenix:html usage +## Phoenix HTML guidelines + +- Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E` +- **Always** use the imported `Phoenix.Component.form/1` and `Phoenix.Component.inputs_for/1` function to build forms. **Never** use `Phoenix.HTML.form_for` or `Phoenix.HTML.inputs_for` as they are outdated +- When building forms **always** use the already imported `Phoenix.Component.to_form/2` (`assign(socket, form: to_form(...))` and `<.form for={@form} id="msg-form">`), then access those forms in the template via `@form[:field]` +- **Always** add unique DOM IDs to key elements (like forms, buttons, etc) when writing templates, these IDs can later be used in tests (`<.form for={@form} id="product-form">`) +- For "app wide" template imports, you can import/alias into the `my_app_web.ex`'s `html_helpers` block, so they will be available to all LiveViews, LiveComponent's, and all modules that do `use MyAppWeb, :html` (replace "my_app" by the actual app name) + +- Elixir supports `if/else` but **does NOT support `if/else if` or `if/elsif`**. **Never use `else if` or `elseif` in Elixir**, **always** use `cond` or `case` for multiple conditionals. + + **Never do this (invalid)**: + + <%= if condition do %> + ... + <% else if other_condition %> + ... + <% end %> + + Instead **always** do this: + + <%= cond do %> + <% condition -> %> + ... + <% condition2 -> %> + ... + <% true -> %> + ... + <% end %> + +- HEEx require special tag annotation if you want to insert literal curly's like `{` or `}`. If you want to show a textual code snippet on the page in a `
` or `` block you *must* annotate the parent tag with `phx-no-curly-interpolation`:
+
+      
+        let obj = {key: "val"}
+      
+
+  Within `phx-no-curly-interpolation` annotated tags, you can use `{` and `}` without escaping them, and dynamic Elixir expressions can still be used with `<%= ... %>` syntax
+
+- HEEx class attrs support lists, but you must **always** use list `[...]` syntax. You can use the class list syntax to conditionally add classes, **always do this for multiple class values**:
+
+      Text
+
+  and **always** wrap `if`'s inside `{...}` expressions with parens, like done above (`if(@other_condition, do: "...", else: "...")`)
+
+  and **never** do this, since it's invalid (note the missing `[` and `]`):
+
+       ...
+      => Raises compile syntax error on invalid HEEx attr syntax
+
+- **Never** use `<% Enum.each %>` or non-for comprehensions for generating template content, instead **always** use `<%= for item <- @collection do %>`
+- HEEx HTML comments use `<%!-- comment --%>`. **Always** use the HEEx HTML comment syntax for template comments (`<%!-- comment --%>`)
+- HEEx allows interpolation via `{...}` and `<%= ... %>`, but the `<%= %>` **only** works within tag bodies. **Always** use the `{...}` syntax for interpolation within tag attributes, and for interpolation of values within tag bodies. **Always** interpolate block constructs (if, cond, case, for) within tag bodies using `<%= ... %>`.
+
+  **Always** do this:
+
+      
+ {@my_assign} + <%= if @some_block_condition do %> + {@another_assign} + <% end %> +
+ + and **Never** do this – the program will terminate with a syntax error: + + <%!-- THIS IS INVALID NEVER EVER DO THIS --%> +
+ {if @invalid_block_construct do} + {end} +
+ + + +## phoenix:liveview usage +## Phoenix LiveView guidelines + +- **Never** use the deprecated `live_redirect` and `live_patch` functions, instead **always** use the `<.link navigate={href}>` and `<.link patch={href}>` in templates, and `push_navigate` and `push_patch` functions LiveViews +- **Avoid LiveComponent's** unless you have a strong, specific need for them +- LiveViews should be named like `AppWeb.WeatherLive`, with a `Live` suffix. When you go to add LiveView routes to the router, the default `:browser` scope is **already aliased** with the `AppWeb` module, so you can just do `live "/weather", WeatherLive` + +### LiveView streams + +- **Always** use LiveView streams for collections for assigning regular lists to avoid memory ballooning and runtime termination with the following operations: + - basic append of N items - `stream(socket, :messages, [new_msg])` + - resetting stream with new items - `stream(socket, :messages, [new_msg], reset: true)` (e.g. for filtering items) + - prepend to stream - `stream(socket, :messages, [new_msg], at: -1)` + - deleting items - `stream_delete(socket, :messages, msg)` + +- When using the `stream/3` interfaces in the LiveView, the LiveView template must 1) always set `phx-update="stream"` on the parent element, with a DOM id on the parent element like `id="messages"` and 2) consume the `@streams.stream_name` collection and use the id as the DOM id for each child. For a call like `stream(socket, :messages, [new_msg])` in the LiveView, the template would be: + +
+
+ {msg.text} +
+
+ +- LiveView streams are *not* enumerable, so you cannot use `Enum.filter/2` or `Enum.reject/2` on them. Instead, if you want to filter, prune, or refresh a list of items on the UI, you **must refetch the data and re-stream the entire stream collection, passing reset: true**: + + def handle_event("filter", %{"filter" => filter}, socket) do + # re-fetch the messages based on the filter + messages = list_messages(filter) + + {:noreply, + socket + |> assign(:messages_empty?, messages == []) + # reset the stream with the new messages + |> stream(:messages, messages, reset: true)} + end + +- LiveView streams *do not support counting or empty states*. If you need to display a count, you must track it using a separate assign. For empty states, you can use Tailwind classes: + +
+ +
+ {task.name} +
+
+ + The above only works if the empty state is the only HTML block alongside the stream for-comprehension. + +- When updating an assign that should change content inside any streamed item(s), you MUST re-stream the items + along with the updated assign: + + def handle_event("edit_message", %{"message_id" => message_id}, socket) do + message = Chat.get_message!(message_id) + edit_form = to_form(Chat.change_message(message, %{content: message.content})) + + # re-insert message so @editing_message_id toggle logic takes effect for that stream item + {:noreply, + socket + |> stream_insert(:messages, message) + |> assign(:editing_message_id, String.to_integer(message_id)) + |> assign(:edit_form, edit_form)} + end + + And in the template: + +
+
+ {message.username} + <%= if @editing_message_id == message.id do %> + <%!-- Edit mode --%> + <.form for={@edit_form} id="edit-form-#{message.id}" phx-submit="save_edit"> + ... + + <% end %> +
+
+ +- **Never** use the deprecated `phx-update="append"` or `phx-update="prepend"` for collections + +### LiveView JavaScript interop + +- Remember anytime you use `phx-hook="MyHook"` and that JS hook manages its own DOM, you **must** also set the `phx-update="ignore"` attribute +- **Always** provide an unique DOM id alongside `phx-hook` otherwise a compiler error will be raised + +LiveView hooks come in two flavors, 1) colocated js hooks for "inline" scripts defined inside HEEx, +and 2) external `phx-hook` annotations where JavaScript object literals are defined and passed to the `LiveSocket` constructor. + +#### Inline colocated js hooks + +**Never** write raw embedded ` + +- colocated hooks are automatically integrated into the app.js bundle +- colocated hooks names **MUST ALWAYS** start with a `.` prefix, i.e. `.PhoneNumber` + +#### External phx-hook + +External JS hooks (`
`) must be placed in `assets/js/` and passed to the +LiveSocket constructor: + + const MyHook = { + mounted() { ... } + } + let liveSocket = new LiveSocket("/live", Socket, { + hooks: { MyHook } + }); + +#### Pushing events between client and server + +Use LiveView's `push_event/3` when you need to push events/data to the client for a phx-hook to handle. +**Always** return or rebind the socket on `push_event/3` when pushing events: + + # re-bind socket so we maintain event state to be pushed + socket = push_event(socket, "my_event", %{...}) + + # or return the modified socket directly: + def handle_event("some_event", _, socket) do + {:noreply, push_event(socket, "my_event", %{...})} + end + +Pushed events can then be picked up in a JS hook with `this.handleEvent`: + + mounted() { + this.handleEvent("my_event", data => console.log("from server:", data)); + } + +Clients can also push an event to the server and receive a reply with `this.pushEvent`: + + mounted() { + this.el.addEventListener("click", e => { + this.pushEvent("my_event", { one: 1 }, reply => console.log("got reply from server:", reply)); + }) + } + +Where the server handled it via: + + def handle_event("my_event", %{"one" => 1}, socket) do + {:reply, %{two: 2}, socket} + end + +### LiveView tests + +- `Phoenix.LiveViewTest` module and `LazyHTML` (included) for making your assertions +- Form tests are driven by `Phoenix.LiveViewTest`'s `render_submit/2` and `render_change/2` functions +- Come up with a step-by-step test plan that splits major test cases into small, isolated files. You may start with simpler tests that verify content exists, gradually add interaction tests +- **Always reference the key element IDs you added in the LiveView templates in your tests** for `Phoenix.LiveViewTest` functions like `element/2`, `has_element/2`, selectors, etc +- **Never** tests again raw HTML, **always** use `element/2`, `has_element/2`, and similar: `assert has_element?(view, "#my-form")` +- Instead of relying on testing text content, which can change, favor testing for the presence of key elements +- Focus on testing outcomes rather than implementation details +- Be aware that `Phoenix.Component` functions like `<.form>` might produce different HTML than expected. Test against the output HTML structure, not your mental model of what you expect it to be +- When facing test failures with element selectors, add debug statements to print the actual HTML, but use `LazyHTML` selectors to limit the output, ie: + + html = render(view) + document = LazyHTML.from_fragment(html) + matches = LazyHTML.filter(document, "your-complex-selector") + IO.inspect(matches, label: "Matches") + +### Form handling + +#### Creating a form from params + +If you want to create a form based on `handle_event` params: + + def handle_event("submitted", params, socket) do + {:noreply, assign(socket, form: to_form(params))} + end + +When you pass a map to `to_form/1`, it assumes said map contains the form params, which are expected to have string keys. + +You can also specify a name to nest the params: + + def handle_event("submitted", %{"user" => user_params}, socket) do + {:noreply, assign(socket, form: to_form(user_params, as: :user))} + end + +#### Creating a form from changesets + +When using changesets, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema: + + defmodule MyApp.Users.User do + use Ecto.Schema + ... + end + +And then you create a changeset that you pass to `to_form`: + + %MyApp.Users.User{} + |> Ecto.Changeset.change() + |> to_form() + +Once the form is submitted, the params will be available under `%{"user" => user_params}`. + +In the template, the form form assign can be passed to the `<.form>` function component: + + <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save"> + <.input field={@form[:field]} type="text" /> + + +Always give the form an explicit, unique DOM ID, like `id="todo-form"`. + +#### Avoiding form errors + +**Always** use a form assigned via `to_form/2` in the LiveView, and the `<.input>` component in the template. In the template **always access forms this**: + + <%!-- ALWAYS do this (valid) --%> + <.form for={@form} id="my-form"> + <.input field={@form[:field]} type="text" /> + + +And **never** do this: + + <%!-- NEVER do this (invalid) --%> + <.form for={@changeset} id="my-form"> + <.input field={@changeset[:field]} type="text" /> + + +- You are FORBIDDEN from accessing the changeset in the template as it will cause errors +- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset + + + +## phoenix:phoenix usage +## Phoenix guidelines + +- Remember Phoenix router `scope` blocks include an optional alias which is prefixed for all routes within the scope. **Always** be mindful of this when creating routes within a scope to avoid duplicate module prefixes. + +- You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie: + + scope "/admin", AppWeb.Admin do + pipe_through :browser + + live "/users", UserLive, :index + end + + the UserLive route would point to the `AppWeb.Admin.UserLive` module + +- `Phoenix.View` no longer is needed or included with Phoenix, don't use it + + + +## ash usage +_A declarative, extensible framework for building Elixir applications._ + +# Rules for working with Ash + +## Understanding Ash + +Ash is an opinionated, composable framework for building applications in Elixir. It provides a declarative approach to modeling your domain with resources at the center. Read documentation *before* attempting to use its features. Do not assume that you have prior knowledge of the framework or its conventions. + + + + +## ash:actions usage +[ash:actions usage rules](deps/ash/usage-rules/actions.md) + + +## ash:migrations usage +[ash:migrations usage rules](deps/ash/usage-rules/migrations.md) + + +## ash:testing usage +[ash:testing usage rules](deps/ash/usage-rules/testing.md) + + +## tribes_plugin_api usage +_tribes_plugin_api_ + +# Tribes Plugin API Usage Rules + +These rules apply when implementing the public plugin contract from +`tribes_plugin_api`. For deeper context from a plugin checkout, see +[`../tribes/docs/plugins.md`](../tribes/docs/plugins.md); from this package +source directory, the same document is at `../docs/plugins.md`. + +## Entry Modules + +- Implement the runtime contract with `Tribes.Plugin` or `Tribes.Plugin.Base`. +- Prefer `use Tribes.Plugin.Base, otp_app: :your_plugin` for normal plugins; it + reads `manifest.json` and fills the manifest-backed spec fields. +- Keep the host-facing entry module under `Tribes.Plugins.*.Plugin`. It may be a + thin delegate to the plugin application's own module. +- `register/1` must return a `Tribes.Plugin.Spec` struct or a map/struct that + validates into that spec. + +## Spec Discipline + +- Fields mirrored from `manifest.json` must match exactly after capability + normalization: `name`, `version`, `provider_priority`, `provides`, + `requires`, and `enhances_with`. +- Use `%Tribes.Plugin.Spec.NavItem{}` and `%Tribes.Plugin.Spec.Page{}` or maps + with only the documented keys. Unknown keys fail validation. +- Use atom modules for `live_view`, plug modules, hook modules, `ui_components`, + and `ash_domains`; do not pass module names as strings in the runtime spec. +- Keep `children` as valid supervisor child specs and make plugin processes + restartable under normal OTP supervision rules. +- Run `scripts/plugin validate` or `mix tribes.plugin.validate` before relying + on runtime registration behavior. + +## Pages, Layouts, And Auth + +- Use `Tribes.Plugin.Layouts.app` for pages that should render inside host + chrome. +- Use `Tribes.Plugin.LiveUserAuth` on plugin LiveViews when they need host user + context. +- Avoid `use TribesWeb, :live_view` in standalone release-facing plugin code; + use `Phoenix.LiveView` and public plugin/UI APIs instead. + +## Config Schema + +- `config_schema` is for small admin-editable runtime defaults rendered by the + host UI and stored through `Tribes.ConfigStore`. +- Group IDs and setting keys must use the identifier format accepted by the + validator: lowercase segments with letters, digits, underscores, and dots. +- Supported setting types are `:string`, `:text`, `:boolean`, `:integer`, + `:number`, `:enum`, `:list`, and `:object`. +- Use `options` only with `:enum`, and provide stable stored values rather than + display text as the value. + + + +## tribes usage +_tribes_ + +# Tribes Plugin Development Rules + +These rules are for external Tribes plugin projects that depend on the host +checkout during development. For the full contract, see +[`../tribes/docs/plugins.md`](../tribes/docs/plugins.md) and +[`../tribes/docs/ui.md`](../tribes/docs/ui.md). + +## Plugin Boundaries + +- Treat plugins as separate OTP applications. The host discovers them through + `manifest.json` and a single `Tribes.Plugin` entry module, not by reaching + into plugin internals. +- Keep plugin contributions inside the supported runtime spec fields: + `nav_items`, `pages`, `api_routes`, `plugs`, `children`, `global_js`, + `global_css`, `migrations_path`, `ui_components`, `hooks`, `ash_domains`, and + `config_schema`. +- Do not mutate host routers, host Ash domains, endpoint config, or host + supervision trees directly from plugin code. +- Plugin-owned pages and API routes should live under plugin-owned paths. Avoid + taking over unrelated host sections. + +## Manifest And Runtime Spec + +- `manifest.json` is the static build/runtime contract. Keep `name`, + `version`, `entry_module`, `host_api`, `otp_app`, `provides`, `requires`, and + `enhances_with` aligned with the runtime spec returned by `register/1`. +- `entry_module` must be under `Tribes.Plugins.*.Plugin` and end in `.Plugin`. +- Capability versions are discrete breaking-change markers such as `ui@1` or + `ecto@1`, not semver. +- Use `requires` for hard dependencies and `enhances_with` for optional + integrations that the plugin can run without. +- Run `scripts/plugin validate` after changing `manifest.json` or the runtime + plugin spec. + +## UI And Assets + +- Plugin LiveViews that use host chrome should render with + `Tribes.Plugin.Layouts.app` and keep `ui@1` in `manifest.json` `requires`. +- Consumers should target the `ui@1` facade with `use Tribes.UI` or + `import Tribes.UI.Components`, not a concrete provider module. +- Declare browser assets in `manifest.json` under `assets.global_js` and + `assets.global_css`; the host serves them through the plugin asset surface. +- Keep CSS selectors scoped to the plugin, normally with a plugin-specific root + class. + +## Runtime Config + +- Use plugin OTP app env only for boot-time wiring that is genuinely static. +- For mutable runtime settings, use `Tribes.ConfigStore` and expose small + editable defaults through `config_schema` in the plugin spec. +- `config_schema` is validated by the host and rendered by Tribes in the admin + settings UI. It describes fields only; values are stored as ConfigStore + overrides when an admin saves them. +- If `config_schema.namespace` is omitted, the host uses + `plugin.`. +- Plugins should read ConfigStore values with the same defaults declared in the + schema, because saving a value equal to the default deletes the override. +- Do not expose secrets, node-local paths, ports, database URLs, TLS material, + package selection, or deployment state through `config_schema`. + +## Synced Data + +- If a plugin adds Ash resources that should replicate across the cluster, add + them deliberately to the plugin spec `ash_domains` and document replication + semantics in the plugin contract docs. +- Use `AshNostrSync` only for resources whose persisted state is meant to be + cluster-synced. Do not enable it as a default on every plugin resource. + + + diff --git a/mix.exs b/mix.exs index cd9186a..3ba4840 100644 --- a/mix.exs +++ b/mix.exs @@ -9,7 +9,8 @@ defmodule Sender.MixProject do elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), - aliases: aliases() + aliases: aliases(), + usage_rules: usage_rules() ] end @@ -36,6 +37,10 @@ defmodule Sender.MixProject do # For CI or standalone development, this can be replaced with a published # package once tribes_plugin_api is released. {:tribes_plugin_api, path: "../tribes/tribes_plugin_api", runtime: false}, + {:tribes_plugin, path: "../tribes-plugin-new", only: [:dev, :test], runtime: false}, + {:igniter, "~> 0.7", only: [:dev, :test], runtime: false}, + {:ash, "~> 3.0", only: [:dev, :test], runtime: false}, + {:usage_rules, "~> 1.2", only: :dev}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:lazy_html, ">= 0.1.0", only: :test}, {:phoenix, "~> 1.8"}, @@ -61,4 +66,25 @@ defmodule Sender.MixProject do ] ] end + + defp usage_rules do + [ + file: "AGENTS.md", + usage_rules: [ + {:usage_rules, sub_rules: []}, + {"usage_rules:elixir", main: false}, + {"usage_rules:otp", main: false}, + "phoenix:ecto", + "phoenix:html", + "phoenix:liveview", + "phoenix:phoenix", + {:ash, sub_rules: []}, + {"ash:actions", link: :markdown, main: false}, + {"ash:migrations", link: :markdown, main: false}, + {"ash:testing", link: :markdown, main: false}, + {:tribes_plugin_api, sub_rules: []}, + {:tribes, sub_rules: []} + ] + ] + end end diff --git a/mix.lock b/mix.lock index 86ac64e..4037cd0 100644 --- a/mix.lock +++ b/mix.lock @@ -43,8 +43,10 @@ "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"}, "fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"}, "gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"}, + "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "igniter": {:hex, :igniter, "0.7.9", "8c573440b8127fd80be8220fb197e7422317a81072054fcc0b336029f035a416", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "123513d09f3af149db851aad8492b5b49f861d2c466a72031b2a0cbd9f45526f"}, "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, @@ -57,6 +59,7 @@ "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, + "owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"}, "parrhesia": {:hex, :parrhesia, "0.12.0", "67f33b62a6d7d32ce8d801e3dd09f3e8f46d82a21e9b15dfda6a161d2c7f7e09", [:mix], [{:bandit, "~> 1.5", [hex: :bandit, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:lib_secp256k1, "~> 0.7", [hex: :lib_secp256k1, repo: "hexpm", optional: false]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus, "~> 1.1", [hex: :telemetry_metrics_prometheus, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}, {:websockex, "~> 0.4", [hex: :websockex, repo: "hexpm", optional: false]}], "hexpm", "e91f757972746c73f888706cb084ba745ebf1a602a0f2f6a7c380555c6066cc3"}, "phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"}, @@ -75,8 +78,11 @@ "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, "reactor": {:hex, :reactor, "1.0.1", "ca3b5cf3c04ec8441e67ea2625d0294939822060b1bfd00ffdaaf75b7682d991", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "3497db2b204c9a3cabdaf1b26d2405df1dfbb138ce0ce50e616e9db19fec0043"}, "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, + "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, + "sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"}, "spark": {:hex, :spark, "2.6.1", "b0100216d3883c6a281cb2434af45afbd808695aadb034923cbaf7d8a2ba46ab", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "77bbefa5263bb6b70e1195bc0fc662ddb8ef5937a356a77ae072e56983ad13f0"}, + "spitfire": {:hex, :spitfire, "0.3.11", "79dfcb033762470de472c1c26ea2b4e3aca74700c685dbffd9a13466272c323d", [:mix], [], "hexpm", "eb6e2dadf63214e8bfe65ca9788cef2b03b01027365d78d3c0e3d9ebd3d5b7b4"}, "splode": {:hex, :splode, "0.3.1", "9843c54f84f71b7833fec3f0be06c3cfb5be6b35960ee195ea4fad84b1c25030", [:mix], [], "hexpm", "8f2309b6ec2ecbb01435656429ed1d9ed04ba28797a3280c3b0d1217018ecfbd"}, "stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"}, "swoosh": {:hex, :swoosh, "1.25.0", "d60dcba6d1ce538b1994f8712a3d55bc9519ffba4654cc4665a75683881d11dd", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c59db3d838b595b95954a3d0a13782e56881cecfe7ba7b793b1a1a6775273a6e"}, @@ -85,8 +91,10 @@ "telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"}, "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"}, "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, + "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, + "usage_rules": {:hex, :usage_rules, "1.2.6", "a7b3f8d6e5d265701139d5714749c37c54bb82230a4c51ec54a12a1e4769b9d1", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "608411b9876a16a9d62a427dbaf42faf458e4cd0a508b3bd7e5ee71502073582"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"}, "websockex": {:hex, :websockex, "0.5.1", "9de28d37bbe34f371eb46e29b79c94c94fff79f93c960d842fbf447253558eb4", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8ef39576ed56bc3804c9cd8626f8b5d6b5721848d2726c0ccd4f05385a3c9f14"}, diff --git a/scripts/plugin b/scripts/plugin old mode 100644 new mode 100755 index 46ca82c..845e19f --- a/scripts/plugin +++ b/scripts/plugin @@ -7,6 +7,7 @@ Usage: plugin validate plugin test [mix test args...] plugin precommit [mix precommit args...] + plugin smoke plugin shell EOF } @@ -22,6 +23,52 @@ host_root="${TRIBES_HOST_ROOT:-$plugin_root/../tribes}" host_root="$(cd "$host_root" && pwd -P)" host_script="$host_root/scripts/plugin" +json_field() { + local field="$1" + + FIELD="$field" mix run --no-start -e ' + field = System.fetch_env!("FIELD") + manifest = "manifest.json" |> File.read!() |> JSON.decode!() + value = Map.fetch!(manifest, field) + IO.write(value) + ' +} + +run_smoke() { + cd "$plugin_root" + + mix compile + + local otp_app + local entry_module + otp_app="$(json_field otp_app)" + entry_module="$(json_field entry_module)" + + local beam_path="_build/dev/lib/$otp_app/ebin/Elixir.$entry_module.beam" + [[ -f "$beam_path" ]] || fail "expected entry module beam at $beam_path" + + cd "$host_root" + PLUGIN_ROOT="$plugin_root" ENTRY_MODULE="$entry_module" mix run --no-start -e ' + plugin_root = System.fetch_env!("PLUGIN_ROOT") + entry_module = System.fetch_env!("ENTRY_MODULE") + + plugin_root + |> Path.join("_build/dev/lib/*/ebin") + |> Path.wildcard() + |> Enum.each(&(:code.add_patha(String.to_charlist(&1)))) + + module = String.to_atom("Elixir." <> entry_module) + + case Code.ensure_loaded(module) do + {:module, ^module} -> + IO.puts("Loaded #{entry_module}") + + other -> + raise "failed to load #{entry_module}: #{inspect(other)}" + end + ' +} + command_name="${1:-}" if [[ -z "$command_name" ]]; then usage @@ -30,7 +77,7 @@ fi shift case "$command_name" in - validate | test | precommit | shell) ;; + validate | test | precommit | smoke | shell) ;; -h | --help | help) usage exit 0 @@ -43,6 +90,11 @@ esac [[ -x "$host_script" || -f "$host_script" ]] || fail "expected host plugin script at $host_script" +if [[ "$command_name" == "smoke" ]]; then + run_smoke "$@" + exit 0 +fi + if [[ "${DEVENV_ROOT:-}" == "$plugin_root" ]]; then command -v devenv >/dev/null 2>&1 || fail "devenv is required when running from the plugin devenv shell" cd "$host_root"