Packages
A config-driven dev tool for Elixir projects to manage AGENTS.md files and agent skills from dependencies
Current section
42 Versions
Jump to
Current section
42 Versions
Compare versions
7
files changed
+217
additions
-31
deletions
| @@ -5,6 +5,19 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline | |
| 5 5 | |
| 6 6 | <!-- changelog --> |
| 7 7 | |
| 8 | + ## v0.1.8 (2025-06-24) |
| 9 | + |
| 10 | + |
| 11 | + |
| 12 | + |
| 13 | + ### Improvements: |
| 14 | + |
| 15 | + * remove a confusing usage rule on list purposes by Zach Daniel |
| 16 | + |
| 17 | + * add a usage rule about indexing lists/enumerables by Zach Daniel |
| 18 | + |
| 19 | + * add builtin usage rules by Zach Daniel |
| 20 | + |
| 8 21 | ## v0.1.7 (2025-06-23) |
| @@ -46,7 +46,9 @@ This will create a folder called `rules`, with a file per package that has a `us | |
| 46 46 | to those from you rules file. |
| 47 47 | |
| 48 48 | ```sh |
| 49 | - mix usage_rules.sync CLAUDE.md --all --link-to-folder docs |
| 49 | + mix usage_rules.sync CLAUDE.md --all \ |
| 50 | + --link-to-folder docs \ |
| 51 | + --builtins elixir,otp |
| 50 52 | ``` |
| 51 53 | |
| 52 54 | #### Using deps folder |
| @@ -6,16 +6,17 @@ | |
| 6 6 | {<<"GitHub">>,<<"https://github.com/ash-project/usage_rules">>}, |
| 7 7 | {<<"Website">>,<<"https://ash-hq.org">>}]}. |
| 8 8 | {<<"name">>,<<"usage_rules">>}. |
| 9 | - {<<"version">>,<<"0.1.7">>}. |
| 9 | + {<<"version">>,<<"0.1.8">>}. |
| 10 10 | {<<"description">>, |
| 11 11 | <<"A dev tool for Elixir projects to gather LLM usage rules from dependencies">>}. |
| 12 12 | {<<"elixir">>,<<"~> 1.18">>}. |
| 13 13 | {<<"files">>, |
| 14 14 | [<<"lib">>,<<"lib/mix">>,<<"lib/mix/tasks">>, |
| 15 15 | <<"lib/mix/tasks/usage_rules.install.ex">>, |
| 16 | - <<"lib/mix/tasks/usage_rules.sync.ex">>,<<"lib/usage_rules.ex">>, |
| 17 | - <<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>, |
| 18 | - <<"CHANGELOG.md">>]}. |
| 16 | + <<"lib/mix/tasks/usage_rules.sync.ex">>,<<"lib/usage_rules.ex">>,<<"priv">>, |
| 17 | + <<"priv/builtins">>,<<"priv/builtins/elixir.md">>, |
| 18 | + <<"priv/builtins/otp.md">>,<<".formatter.exs">>,<<"mix.exs">>, |
| 19 | + <<"README.md">>,<<"LICENSE">>,<<"CHANGELOG.md">>]}. |
| 19 20 | {<<"app">>,<<"usage_rules">>}. |
| 20 21 | {<<"licenses">>,[<<"MIT">>]}. |
| 21 22 | {<<"requirements">>, |
| @@ -23,6 +23,7 @@ defmodule Mix.Tasks.UsageRules.Sync.Docs do | |
| 23 23 | * `--remove` - Remove specified packages from the target file instead of adding them |
| 24 24 | * `--link-to-folder <folder>` - Save usage rules for each package in separate files within the specified folder and create links to them |
| 25 25 | * `--link-style <style>` - Style of links to create when using --link-to-folder (markdown|at). Defaults to 'markdown' |
| 26 | + * `--builtins <builtins>` - Include built-in usage rules (comma-separated: elixir,otp) |
| 26 27 | |
| 27 28 | ## Examples |
| 28 29 | |
| @@ -80,6 +81,11 @@ defmodule Mix.Tasks.UsageRules.Sync.Docs do | |
| 80 81 | ```sh |
| 81 82 | mix usage_rules.sync CLAUDE.md ash phoenix --remove --link-to-folder rules |
| 82 83 | ``` |
| 84 | + |
| 85 | + Include built-in Elixir and OTP usage rules: |
| 86 | + ```sh |
| 87 | + mix usage_rules.sync CLAUDE.md --all --link-to-folder deps --builtins elixir,otp |
| 88 | + ``` |
| 83 89 | """ |
| 84 90 | end |
| 85 91 | end |
| @@ -108,7 +114,8 @@ if Code.ensure_loaded?(Igniter) do | |
| 108 114 | list: :boolean, |
| 109 115 | remove: :boolean, |
| 110 116 | link_to_folder: :string, |
| 111 | - link_style: :string |
| 117 | + link_style: :string, |
| 118 | + builtins: :string |
| 112 119 | ] |
| 113 120 | } |
| 114 121 | end |
| @@ -149,8 +156,19 @@ if Code.ensure_loaded?(Igniter) do | |
| 149 156 | link_to_folder = igniter.args.options[:link_to_folder] |
| 150 157 | link_style = igniter.args.options[:link_style] || "markdown" |
| 151 158 | provided_packages = igniter.args.positional.packages |
| 159 | + builtins = parse_builtins(igniter.args.options[:builtins]) |
| 152 160 | |
| 153 161 | cond do |
| 162 | + # If --builtins contains invalid values, add error |
| 163 | + igniter.args.options[:builtins] && |
| 164 | + parse_invalid_builtins(igniter.args.options[:builtins]) != [] -> |
| 165 | + invalid = parse_invalid_builtins(igniter.args.options[:builtins]) |
| 166 | + |
| 167 | + Igniter.add_issue( |
| 168 | + igniter, |
| 169 | + "Invalid builtins: #{Enum.join(invalid, ", ")}. Valid options are: elixir, otp" |
| 170 | + ) |
| 171 | + |
| 154 172 | # If --link-style is used with invalid value, add error |
| 155 173 | link_style && link_style not in ["markdown", "at"] -> |
| 156 174 | Igniter.add_issue(igniter, "--link-style must be either 'markdown' or 'at'") |
| @@ -193,7 +211,7 @@ if Code.ensure_loaded?(Igniter) do | |
| 193 211 | |
| 194 212 | # Handle --all option |
| 195 213 | all_option -> |
| 196 | - handle_all_option(igniter, all_deps, link_to_folder, link_style) |
| 214 | + handle_all_option(igniter, all_deps, link_to_folder, link_style, builtins) |
| 197 215 | |
| 198 216 | # Handle --list option |
| 199 217 | list_option -> |
| @@ -206,11 +224,48 @@ if Code.ensure_loaded?(Igniter) do | |
| 206 224 | all_deps, |
| 207 225 | provided_packages, |
| 208 226 | link_to_folder, |
| 209 | - link_style |
| 227 | + link_style, |
| 228 | + builtins |
| 210 229 | ) |
| 211 230 | end |
| 212 231 | end |
| 213 232 | |
| 233 | + defp parse_builtins(nil), do: [] |
| 234 | + |
| 235 | + defp parse_builtins(builtins_string) do |
| 236 | + builtins_string |
| 237 | + |> String.split(",") |
| 238 | + |> Enum.map(&String.trim/1) |
| 239 | + |> Enum.filter(fn builtin -> |
| 240 | + builtin in ["elixir", "otp"] |
| 241 | + end) |
| 242 | + end |
| 243 | + |
| 244 | + defp parse_invalid_builtins(nil), do: [] |
| 245 | + |
| 246 | + defp parse_invalid_builtins(builtins_string) do |
| 247 | + builtins_string |
| 248 | + |> String.split(",") |
| 249 | + |> Enum.map(&String.trim/1) |
| 250 | + |> Enum.reject(fn builtin -> |
| 251 | + builtin in ["", "elixir", "otp"] |
| 252 | + end) |
| 253 | + end |
| 254 | + |
| 255 | + defp get_builtin_contents(builtins) do |
| 256 | + builtins |
| 257 | + |> Enum.map(fn builtin -> |
| 258 | + builtin_path = Path.join([:code.priv_dir(:usage_rules), "builtins", "#{builtin}.md"]) |
| 259 | + content = File.read!(builtin_path) |
| 260 | + |
| 261 | + {String.to_atom(builtin), |
| 262 | + "<!-- #{builtin}-start -->\n" <> |
| 263 | + "## #{builtin} usage\n" <> |
| 264 | + content <> |
| 265 | + "\n<!-- #{builtin}-end -->"} |
| 266 | + end) |
| 267 | + end |
| 268 | + |
| 214 269 | defp get_deps_from_igniter(igniter) do |
| 215 270 | if igniter.assigns[:test_mode?] do |
| 216 271 | igniter.rewrite.sources |
| @@ -265,7 +320,7 @@ if Code.ensure_loaded?(Igniter) do | |
| 265 320 | """) |
| 266 321 | end |
| 267 322 | |
| 268 | - defp handle_all_option(igniter, all_deps, link_to_folder, link_style) do |
| 323 | + defp handle_all_option(igniter, all_deps, link_to_folder, link_style, builtins) do |
| 269 324 | all_packages_with_rules = get_packages_with_usage_rules(igniter, all_deps) |
| 270 325 | |
| 271 326 | igniter |
| @@ -277,7 +332,16 @@ if Code.ensure_loaded?(Igniter) do | |
| 277 332 | Igniter.add_notice(acc, "Including usage rules for: #{name}") |
| 278 333 | end) |
| 279 334 | end) |
| 280 | - |> generate_usage_rules_file(all_packages_with_rules, link_to_folder, link_style) |
| 335 | + |> maybe_add_builtin_notices(builtins) |
| 336 | + |> generate_usage_rules_file(all_packages_with_rules, link_to_folder, link_style, builtins) |
| 337 | + end |
| 338 | + |
| 339 | + defp maybe_add_builtin_notices(igniter, []), do: igniter |
| 340 | + |
| 341 | + defp maybe_add_builtin_notices(igniter, builtins) do |
| 342 | + Enum.reduce(builtins, igniter, fn builtin, acc -> |
| 343 | + Igniter.add_notice(acc, "Including built-in usage rules for: #{builtin}") |
| 344 | + end) |
| 281 345 | end |
| 282 346 | |
| 283 347 | defp handle_list_option(igniter, all_deps, link_to_folder) do |
| @@ -306,7 +370,8 @@ if Code.ensure_loaded?(Igniter) do | |
| 306 370 | all_deps, |
| 307 371 | provided_packages, |
| 308 372 | link_to_folder, |
| 309 | - link_style |
| 373 | + link_style, |
| 374 | + builtins |
| 310 375 | ) do |
| 311 376 | packages = |
| 312 377 | all_deps |
| @@ -323,7 +388,9 @@ if Code.ensure_loaded?(Igniter) do | |
| 323 388 | end |
| 324 389 | end) |
| 325 390 | |
| 326 | - generate_usage_rules_file(igniter, packages, link_to_folder, link_style) |
| 391 | + igniter |
| 392 | + |> maybe_add_builtin_notices(builtins) |
| 393 | + |> generate_usage_rules_file(packages, link_to_folder, link_style, builtins) |
| 327 394 | end |
| 328 395 | |
| 329 396 | defp handle_remove_packages(igniter, provided_packages, link_to_folder) do |
| @@ -397,15 +464,21 @@ if Code.ensure_loaded?(Igniter) do | |
| 397 464 | end |
| 398 465 | end |
| 399 466 | |
| 400 | - defp generate_usage_rules_file(igniter, packages, link_to_folder, link_style) do |
| 467 | + defp generate_usage_rules_file(igniter, packages, link_to_folder, link_style, builtins) do |
| 401 468 | if link_to_folder do |
| 402 | - generate_usage_rules_with_folder_links(igniter, packages, link_to_folder, link_style) |
| 469 | + generate_usage_rules_with_folder_links( |
| 470 | + igniter, |
| 471 | + packages, |
| 472 | + link_to_folder, |
| 473 | + link_style, |
| 474 | + builtins |
| 475 | + ) |
| 403 476 | else |
| 404 | - generate_usage_rules_inline(igniter, packages) |
| 477 | + generate_usage_rules_inline(igniter, packages, builtins) |
| 405 478 | end |
| 406 479 | end |
| 407 480 | |
| 408 | - defp generate_usage_rules_inline(igniter, packages) do |
| 481 | + defp generate_usage_rules_inline(igniter, packages, builtins) do |
| 409 482 | package_contents = |
| 410 483 | packages |
| 411 484 | |> Enum.map(fn {name, path} -> |
| @@ -424,11 +497,14 @@ if Code.ensure_loaded?(Igniter) do | |
| 424 497 | "\n<!-- #{name}-end -->"} |
| 425 498 | end) |
| 426 499 | |
| 427 | - package_rules_content = Enum.map_join(package_contents, "\n", &elem(&1, 1)) |
| 500 | + builtin_contents = get_builtin_contents(builtins) |
| 501 | + |
| 502 | + all_contents = package_contents ++ builtin_contents |
| 503 | + all_rules_content = Enum.map_join(all_contents, "\n", &elem(&1, 1)) |
| 428 504 | |
| 429 505 | full_contents_for_new_file = |
| 430 506 | "<!-- usage-rules-start -->\n" <> |
| 431 | - package_rules_content <> |
| 507 | + all_rules_content <> |
| 432 508 | "\n<!-- usage-rules-end -->" |
| 433 509 | |
| 434 510 | Igniter.create_or_update_file( |
| @@ -444,9 +520,8 @@ if Code.ensure_loaded?(Igniter) do | |
| 444 520 | "\n<!-- usage-rules-end -->" |
| 445 521 | ]) do |
| 446 522 | [prelude, current_packages_contents, postlude] -> |
| 447 | - Enum.reduce(package_contents, current_packages_contents, fn {name, |
| 448 | - package_content}, |
| 449 | - acc -> |
| 523 | + Enum.reduce(all_contents, current_packages_contents, fn {name, package_content}, |
| 524 | + acc -> |
| 450 525 | case String.split(acc, [ |
| 451 526 | "<!-- #{name}-start -->\n", |
| 452 527 | "\n<!-- #{name}-end -->" |
| @@ -469,7 +544,7 @@ if Code.ensure_loaded?(Igniter) do | |
| 469 544 | _ -> |
| 470 545 | current_contents <> |
| 471 546 | "\n<!-- usage-rules-start -->\n" <> |
| 472 | - package_rules_content <> |
| 547 | + all_rules_content <> |
| 473 548 | "\n<!-- usage-rules-end -->\n" |
| 474 549 | end |
| 475 550 | |
| @@ -482,13 +557,33 @@ if Code.ensure_loaded?(Igniter) do | |
| 482 557 | igniter, |
| 483 558 | packages, |
| 484 559 | folder_name, |
| 485 | - link_style |
| 560 | + link_style, |
| 561 | + builtins |
| 486 562 | ) do |
| 487 563 | # Create individual files for each package in the folder (unless folder is "deps") |
| 488 564 | igniter = |
| 489 565 | if folder_name == "deps" do |
| 490 566 | igniter |
| 491 567 | else |
| 568 | + # Create builtin files in the target folder |
| 569 | + igniter = |
| 570 | + Enum.reduce(builtins, igniter, fn builtin, acc -> |
| 571 | + builtin_source_path = |
| 572 | + Path.join([:code.priv_dir(:usage_rules), "builtins", "#{builtin}.md"]) |
| 573 | + |
| 574 | + builtin_file_path = Path.join(folder_name, "#{builtin}.md") |
| 575 | + content = File.read!(builtin_source_path) |
| 576 | + |
| 577 | + Igniter.create_or_update_file( |
| 578 | + acc, |
| 579 | + builtin_file_path, |
| 580 | + content, |
| 581 | + fn source -> |
| 582 | + Rewrite.Source.update(source, :content, content) |
| 583 | + end |
| 584 | + ) |
| 585 | + end) |
| 586 | + |
| 492 587 | Enum.reduce(packages, igniter, fn {name, path}, acc -> |
| 493 588 | usage_rules_path = Path.join(path, "usage-rules.md") |
| 494 589 | |
| @@ -530,11 +625,37 @@ if Code.ensure_loaded?(Igniter) do | |
| 530 625 | "\n<!-- #{name}-end -->"} |
| 531 626 | end) |
| 532 627 | |
| 533 | - package_rules_content = Enum.map_join(package_contents, "\n", &elem(&1, 1)) |
| 628 | + builtin_contents = |
| 629 | + builtins |
| 630 | + |> Enum.map(fn builtin -> |
| 631 | + link_content = |
| 632 | + case {link_style, folder_name} do |
| 633 | + {"at", "deps"} -> |
| 634 | + "@deps/usage_rules/priv/builtins/#{builtin}.md" |
| 635 | + |
| 636 | + {"at", _} -> |
| 637 | + "@#{folder_name}/#{builtin}.md" |
| 638 | + |
| 639 | + {_, "deps"} -> |
| 640 | + "[#{builtin} usage rules](deps/usage_rules/priv/builtins/#{builtin}.md)" |
| 641 | + |
| 642 | + _ -> |
| 643 | + "[#{builtin} usage rules](#{folder_name}/#{builtin}.md)" |
| 644 | + end |
| 645 | + |
| 646 | + {String.to_atom(builtin), |
| 647 | + "<!-- #{builtin}-start -->\n" <> |
| 648 | + "## #{builtin} usage\n" <> |
| 649 | + link_content <> |
| 650 | + "\n<!-- #{builtin}-end -->"} |
| 651 | + end) |
| 652 | + |
| 653 | + all_contents = package_contents ++ builtin_contents |
| 654 | + all_rules_content = Enum.map_join(all_contents, "\n", &elem(&1, 1)) |
| 534 655 | |
| 535 656 | full_contents_for_new_file = |
| 536 657 | "<!-- usage-rules-start -->\n" <> |
| 537 | - package_rules_content <> |
| 658 | + all_rules_content <> |
| 538 659 | "\n<!-- usage-rules-end -->" |
| 539 660 | |
| 540 661 | Igniter.create_or_update_file( |
| @@ -550,9 +671,8 @@ if Code.ensure_loaded?(Igniter) do | |
| 550 671 | "\n<!-- usage-rules-end -->" |
| 551 672 | ]) do |
| 552 673 | [prelude, current_packages_contents, postlude] -> |
| 553 | - Enum.reduce(package_contents, current_packages_contents, fn {name, |
| 554 | - package_content}, |
| 555 | - acc -> |
| 674 | + Enum.reduce(all_contents, current_packages_contents, fn {name, package_content}, |
| 675 | + acc -> |
| 556 676 | case String.split(acc, [ |
| 557 677 | "<!-- #{name}-start -->\n", |
| 558 678 | "\n<!-- #{name}-end -->" |
| @@ -575,7 +695,7 @@ if Code.ensure_loaded?(Igniter) do | |
| 575 695 | _ -> |
| 576 696 | current_contents <> |
| 577 697 | "\n<!-- usage-rules-start -->\n" <> |
| 578 | - package_rules_content <> |
| 698 | + all_rules_content <> |
| 579 699 | "\n<!-- usage-rules-end -->\n" |
| 580 700 | end |
| @@ -1,7 +1,7 @@ | |
| 1 1 | defmodule UsageRules.MixProject do |
| 2 2 | use Mix.Project |
| 3 3 | |
| 4 | - @version "0.1.7" |
| 4 | + @version "0.1.8" |
| 5 5 | @description """ |
| 6 6 | A dev tool for Elixir projects to gather LLM usage rules from dependencies |
| 7 7 | """ |
| @@ -31,7 +31,7 @@ defmodule UsageRules.MixProject do | |
| 31 31 | name: :usage_rules, |
| 32 32 | licenses: ["MIT"], |
| 33 33 | maintainers: "Zach Daniel", |
| 34 | - files: ~w(lib .formatter.exs mix.exs README* LICENSE* CHANGELOG*), |
| 34 | + files: ~w(lib priv .formatter.exs mix.exs README* LICENSE* CHANGELOG*), |
| 35 35 | links: %{ |
| 36 36 | "GitHub" => @source_url, |
| 37 37 | "Discord" => "https://discord.gg/HTHRaaVPUc", |
Loading more files…