Compare commits

3 Commits

8 changed files with 107 additions and 30 deletions

View File

@@ -27,15 +27,19 @@ mix test
For local development alongside a Tribes checkout:
```bash
# Build plugin code once (host loads BEAM from _build/dev/lib/<otp_app>/ebin)
cd /path/to/your-plugin
mix compile
# Symlink into the host plugins directory
cd /path/to/tribes
ln -s /path/to/your-plugin plugins/your_plugin
# Start Tribes dev server — your plugin loads automatically
# Start Tribes dev server
iex --sname dev -S mix phx.server
```
Edit your plugin source. Phoenix code reloader picks up changes.
When you change plugin Elixir code, re-run `mix compile` in the plugin repo.
## Project Structure
@@ -63,12 +67,17 @@ your_plugin/
```json
{
"name": "your_plugin",
"entry_module": "Tribes.Plugins.YourPlugin.Plugin",
"host_api": "1",
"otp_app": "your_plugin",
"provides": ["some_capability@1"],
"requires": ["ecto@1"],
"enhances_with": ["inference@1"]
}
```
- **entry_module** — must be `Tribes.Plugins.*.Plugin`
- **otp_app** — required and must match `name`
- **provides** — capabilities this plugin makes available
- **requires** — hard dependencies (build fails without them)
- **enhances_with** — optional dependencies (plugin degrades gracefully)
@@ -85,6 +94,14 @@ Three test levels:
Run all: `mix test`
For DB setup/migrations in local development, run:
```bash
mix tribes.migrate
```
This runs Tribes + Parrhesia + plugin migrations via `Tribes.Release`.
## Building for Release
```bash

View File

@@ -58,24 +58,44 @@ defmodule MyPlugin.Plugin do
end
defp manifest_path do
# In a release, manifest.json sits alongside ebin/ in the plugin directory.
# In dev mode (path dep), it's at the project root.
project_manifest = Path.join(__DIR__, "../../manifest.json") |> Path.expand()
candidates =
case :code.priv_dir(:my_plugin) do
{:error, :bad_name} ->
# Dev mode fallback: relative to project root
Path.join(__DIR__, "../../../manifest.json") |> Path.expand()
[project_manifest]
priv_dir ->
priv_dir |> to_string() |> Path.join("../manifest.json") |> Path.expand()
priv_dir = to_string(priv_dir)
[
Path.join(priv_dir, "../manifest.json") |> Path.expand(),
project_manifest
]
end
first_existing_path(candidates) || project_manifest
end
defp migrations_path(manifest) do
if manifest["migrations"] do
candidates =
case :code.priv_dir(:my_plugin) do
{:error, :bad_name} -> nil
priv_dir -> priv_dir |> to_string() |> Path.join("repo/migrations")
{:error, :bad_name} ->
[Path.join(__DIR__, "../../priv/repo/migrations") |> Path.expand()]
priv_dir ->
[
Path.join(to_string(priv_dir), "repo/migrations") |> Path.expand(),
Path.join(__DIR__, "../../priv/repo/migrations") |> Path.expand()
]
end
first_existing_path(candidates)
end
end
defp first_existing_path(paths) do
Enum.find(paths, &File.exists?/1)
end
end

View File

@@ -0,0 +1,5 @@
defmodule Tribes.Plugins.MyPlugin.Plugin do
@moduledoc false
defdelegate register(context), to: MyPlugin.Plugin
end

View File

@@ -2,8 +2,9 @@
"name": "my_plugin",
"version": "0.1.0",
"description": "TODO: Describe what this plugin does",
"entry_module": "MyPlugin.Plugin",
"entry_module": "Tribes.Plugins.MyPlugin.Plugin",
"host_api": "1",
"otp_app": "my_plugin",
"provides": [],
"requires": ["ecto@1"],
"enhances_with": [],

View File

@@ -29,7 +29,7 @@ defmodule MyPlugin.MixProject do
# Host dependency — provides Tribes.Plugin behaviour, types, and test helpers.
#
# For local development alongside a tribes checkout:
{:tribes, path: "../tribes", only: [:dev, :test]},
{:tribes, path: "../tribes", only: [:dev, :test], runtime: false},
#
# For CI or standalone development (when not co-located with tribes):
# {:tribes, github: "your-org/tribes", branch: "master", only: [:dev, :test]},

View File

@@ -45,13 +45,32 @@ if [ -d "test/my_plugin" ]; then
mv "test/my_plugin" "test/$SNAKE"
fi
# Replace in all text files
find . -type f \( -name '*.ex' -o -name '*.exs' -o -name '*.json' -o -name '*.js' -o -name '*.css' -o -name '*.md' -o -name '*.yml' -o -name '*.yaml' -o -name '.formatter.exs' \) -exec \
# Replace in all text files (portable across GNU/BSD sed)
sed_in_place() {
file=$1
if sed --version >/dev/null 2>&1; then
sed -i \
-e "s/my_plugin/$SNAKE/g" \
-e "s/MyPlugin/$MODULE/g" \
-e "s/my-plugin/$SNAKE/g" \
"$file"
else
sed -i '' \
-e "s/my_plugin/$SNAKE/g" \
-e "s/MyPlugin/$MODULE/g" \
-e "s/my-plugin/$SNAKE/g" \
{} +
"$file"
fi
}
while IFS= read -r -d '' file; do
sed_in_place "$file"
done < <(
find . -type f \
\( -name '*.ex' -o -name '*.exs' -o -name '*.json' -o -name '*.js' -o -name '*.css' -o -name '*.md' -o -name '*.yml' -o -name '*.yaml' -o -name '.formatter.exs' \) \
-print0
)
# Rename asset files
for ext in js css; do

View File

@@ -8,7 +8,7 @@ defmodule MyPlugin.ContractTest do
defmodule MyPlugin.ContractTest do
use Tribes.Plugin.ContractTest,
plugin: MyPlugin.Plugin,
plugin: Tribes.Plugins.MyPlugin.Plugin,
otp_app: :my_plugin
end
@@ -17,7 +17,7 @@ defmodule MyPlugin.ContractTest do
use ExUnit.Case, async: true
@plugin MyPlugin.Plugin
@plugin Tribes.Plugins.MyPlugin.Plugin
@manifest_path Path.join(__DIR__, "../manifest.json") |> Path.expand()
setup_all do

View File

@@ -15,7 +15,15 @@ defmodule MyPlugin.ManifestTest do
end
test "has required fields", %{manifest: manifest} do
required = ["name", "version", "entry_module", "host_api", "provides", "requires"]
required = [
"name",
"version",
"entry_module",
"host_api",
"otp_app",
"provides",
"requires"
]
for field <- required do
assert Map.has_key?(manifest, field),
@@ -25,12 +33,17 @@ defmodule MyPlugin.ManifestTest do
test "name matches OTP app name", %{manifest: manifest} do
assert manifest["name"] == "my_plugin"
assert manifest["otp_app"] == manifest["name"]
end
test "entry_module is a valid Elixir module name", %{manifest: manifest} do
test "entry_module uses Tribes.Plugins namespace and Plugin suffix", %{manifest: manifest} do
module_name = manifest["entry_module"]
assert is_binary(module_name)
assert String.starts_with?(module_name, "Elixir.") or not String.contains?(module_name, " ")
assert Regex.match?(
~r/^Tribes\.Plugins\.[A-Z][A-Za-z0-9_]*(\.[A-Z][A-Za-z0-9_]*)*\.Plugin$/,
module_name
)
end
test "provides contains valid capability identifiers", %{manifest: manifest} do
@@ -53,7 +66,9 @@ defmodule MyPlugin.ManifestTest do
test "entry_module matches actual plugin module", %{manifest: manifest} do
module = String.to_atom("Elixir.#{manifest["entry_module"]}")
assert Code.ensure_loaded?(module), "entry_module #{manifest["entry_module"]} must be loadable"
assert Code.ensure_loaded?(module),
"entry_module #{manifest["entry_module"]} must be loadable"
end
end
end