diff --git a/CHANGELOG.md b/CHANGELOG.md index 32b497d9..e74c6e22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Changed + +- Loosened the `pi_bridge` Mix project (and the bundled `packages/fixtures/demo_project` fixture) `elixir:` requirement from `~> 1.20` to `~> 1.16` so legacy projects on Elixir 1.16–1.19 can adopt the bridge. New-project guidance and startup notices still recommend Elixir 1.20+ / OTP 27+ for the compiler's set-theoretic type-system improvements. The bridge now depends on `json_codec ~> 0.1.5`, which carries the same loosened Elixir requirement. + ## 0.6.21 - 2026-06-14 ### Changed diff --git a/README.md b/README.md index e5a8b002..324b1c03 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,8 @@ Feature flags are escape hatches for noisy, sensitive, or experimental environme ## Recommended project stack +For new projects, install Elixir 1.20+ with OTP 27+ when possible. Elixir 1.20 introduced compiler type-system improvements, including gradual set-theoretic types, whole-body type inference, occurrence typing, and richer map typing; pi-elixir still supports older Elixir releases for existing legacy projects. + For new web applications, use Phoenix with Igniter and VibeKit, then add pi-elixir in the project: ```sh diff --git a/packages/bridge/mix.exs b/packages/bridge/mix.exs index a29a7e0b..8cabfdf0 100644 --- a/packages/bridge/mix.exs +++ b/packages/bridge/mix.exs @@ -5,7 +5,7 @@ defmodule PiBridge.MixProject do [ app: :pi_bridge, version: "0.6.21", - elixir: "~> 1.20", + elixir: "~> 1.16", start_permanent: Mix.env() == :prod, description: "BEAM runtime bridge for pi development agents", package: package(), @@ -60,7 +60,7 @@ defmodule PiBridge.MixProject do defp deps do [ {:jason, "~> 1.4"}, - {:json_codec, "~> 0.1.3"}, + {:json_codec, "~> 0.1.5"}, {:ex_ast, "~> 0.12"}, {:req, "~> 0.5"}, {:quackdb, "~> 0.5.4"}, diff --git a/packages/bridge/mix.lock b/packages/bridge/mix.lock index 2c1b7082..ab14a6b4 100644 --- a/packages/bridge/mix.lock +++ b/packages/bridge/mix.lock @@ -25,7 +25,7 @@ "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, - "json_codec": {:hex, :json_codec, "0.1.3", "e4d8bdc8434da7f60cd519455d5b0ce32128fa05dd4049f3c1020cd0eaff4deb", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "3ed072d68de3df45e4d2220db266bf550eaee057e45e544d4ae140bf5726475e"}, + "json_codec": {:hex, :json_codec, "0.1.5", "44532ea8604f3587d0e8b0b33002745ba068c9cb1663d73c1eee49a5e7cd7e78", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6631dd3ce56a567e57ccb7a1129f57bb9206663fbb644c6ee47acdba36933f73"}, "jsv": {:hex, :jsv, "0.19.4", "92cb229f65fe9fb3007128c1dc2e0d901d6d517d959fdbd3f07f1c74658ee5d3", [:mix], [{:abnf_parsec, "~> 2.0", [hex: :abnf_parsec, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:idna, "~> 6.0 or ~> 7.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:texture, "~> 1.0", [hex: :texture, repo: "hexpm", optional: false]}], "hexpm", "3ff30c9f2413364487b03fb8a8434729debe9fe7cfb3d45d04b6c97390389ec4"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "llm_db": {:hex, :llm_db, "2026.5.2", "bec38e9489d552464e76158442b49aa7d798040cf138f803551bc6b037508210", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:dotenvy, "~> 1.1", [hex: :dotenvy, repo: "hexpm", optional: false]}, {:igniter, "~> 0.7", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:toml, "~> 0.7", [hex: :toml, repo: "hexpm", optional: false]}, {:zoi, "~> 0.10", [hex: :zoi, repo: "hexpm", optional: false]}], "hexpm", "8e4eee268d6682320ef8d087b270dfbfdf6db022c2392971e506e0d4d59e6dba"}, diff --git a/packages/extension/skills/elixir/new-project/SKILL.md b/packages/extension/skills/elixir/new-project/SKILL.md index 7ab36265..4c7b254f 100644 --- a/packages/extension/skills/elixir/new-project/SKILL.md +++ b/packages/extension/skills/elixir/new-project/SKILL.md @@ -7,6 +7,8 @@ description: Start or bootstrap a new Elixir project/package using Igniter and V Use this skill when creating or bootstrapping a new Elixir package/project. +For new projects, recommend Elixir 1.20+ with OTP 27+ when possible. Elixir 1.20 introduced compiler type-system improvements, including gradual set-theoretic types, whole-body type inference, occurrence typing, and richer map typing. `pi_bridge` supports Elixir 1.16+ for existing legacy projects, but do not present 1.16–1.19 as the preferred baseline for new work. + ## Default workflow 1. Inspect `/Users/dannote/Development/vibe_kit` for the current installer behavior before starting. diff --git a/packages/extension/src/bridge/startup-info.ts b/packages/extension/src/bridge/startup-info.ts index b0a769e0..35c6a992 100644 --- a/packages/extension/src/bridge/startup-info.ts +++ b/packages/extension/src/bridge/startup-info.ts @@ -1,4 +1,5 @@ import type { BridgeInfo } from '#src/embedded/stdio-process.ts' +import { detectElixirVersion, shouldRecommendElixir120 } from '#src/mix/runtime.ts' import { EXTENSION_VERSION } from '#src/version.ts' import type { ExtensionContext } from '@earendil-works/pi-coding-agent' @@ -15,7 +16,17 @@ export function renderStartupInfo(info: BridgeInfo) { return lines.join('\n') } -export function showStartupInfo(ctx: ExtensionContext, info: BridgeInfo | undefined) { +export function startupElixirVersionRecommendation(cwd: string): string | null { + const version = detectElixirVersion(cwd) + if (!shouldRecommendElixir120(version)) return null + + return `${version?.raw ?? 'Current Elixir'} detected. pi_bridge supports Elixir 1.16+ for legacy projects, but Elixir 1.20+ with OTP 27+ is recommended for new projects because it adds the compiler set-theoretic type system, whole-body type inference, occurrence typing, and richer map typing.` +} + +export function showStartupInfo(ctx: ExtensionContext, info: BridgeInfo | undefined, cwd: string) { + const recommendation = startupElixirVersionRecommendation(cwd) + if (recommendation) ctx.ui.notify(recommendation, 'warning') + if (!info) return ctx.ui.notify(renderStartupInfo(info), 'info') } diff --git a/packages/extension/src/index.ts b/packages/extension/src/index.ts index bbb99cb3..206c28e6 100644 --- a/packages/extension/src/index.ts +++ b/packages/extension/src/index.ts @@ -375,7 +375,7 @@ export default function (pi: ExtensionAPI) { reportConnectionNotice(key, sessionCwd, kind) if (conn) await loadSessionSnapshots(ctx, sessionCwd, conn.url) const info = getBridgeInfo(sessionCwd) - showStartupInfo(ctx, info) + showStartupInfo(ctx, info, sessionCwd) if (flags.plugins()) registerBridgeCommands(pi, info, registeredCommands, resolveElixirCwd) if (flags.plugins()) await sendBridgeEvent(sessionCwd, { diff --git a/packages/extension/src/mix/runtime.ts b/packages/extension/src/mix/runtime.ts index 14ae928a..92ea463d 100644 --- a/packages/extension/src/mix/runtime.ts +++ b/packages/extension/src/mix/runtime.ts @@ -1,5 +1,12 @@ import * as childProcess from 'node:child_process' +export interface ElixirVersion { + major: number + minor: number + patch: number | null + raw: string +} + function commandExists(command: string): boolean { const result = childProcess.spawnSync(command, ['--version'], { stdio: 'ignore', @@ -9,6 +16,38 @@ function commandExists(command: string): boolean { return result?.status === 0 } +export function detectElixirVersion(cwd = process.cwd()): ElixirVersion | null { + const result = childProcess.spawnSync('elixir', ['--version'], { + cwd, + encoding: 'utf8', + timeout: 3_000 + }) + + if (result.status !== 0) return null + + const output = [result.stdout, result.stderr] + .filter((value): value is string => typeof value === 'string') + .join('\n') + const raw = output + .split('\n') + .map((line) => line.trim()) + .find((line) => line.startsWith('Elixir ')) + const match = raw?.match(/^Elixir\s+(\d+)\.(\d+)(?:\.(\d+))?/) + if (!raw || !match) return null + + return { + major: Number(match[1]), + minor: Number(match[2]), + patch: match[3] ? Number(match[3]) : null, + raw + } +} + +export function shouldRecommendElixir120(version: ElixirVersion | null): boolean { + if (!version) return false + return version.major < 1 || (version.major === 1 && version.minor < 20) +} + export function elixirRuntimeProblem(): string | null { if (!commandExists('elixir')) { return 'Elixir is not installed or not available on PATH. Install Elixir/OTP before using pi-elixir BEAM tools.' diff --git a/packages/extension/test/mix/runtime.test.ts b/packages/extension/test/mix/runtime.test.ts index 37430dee..aae4f2ff 100644 --- a/packages/extension/test/mix/runtime.test.ts +++ b/packages/extension/test/mix/runtime.test.ts @@ -6,7 +6,11 @@ vi.mock('node:child_process', () => ({ import * as childProcess from 'node:child_process' -import { elixirRuntimeProblem } from '#src/mix/runtime.ts' +import { + detectElixirVersion, + elixirRuntimeProblem, + shouldRecommendElixir120 +} from '#src/mix/runtime.ts' describe('elixirRuntimeProblem', () => { beforeEach(() => { @@ -36,4 +40,23 @@ describe('elixirRuntimeProblem', () => { expect(elixirRuntimeProblem()).toContain('Mix is not available') }) + + it('detects Elixir versions for startup recommendations', () => { + vi.mocked(childProcess.spawnSync).mockReturnValue({ + status: 0, + stdout: 'Erlang/OTP 27 [erts-15.0]\nElixir 1.19.3\n', + stderr: '' + } as childProcess.SpawnSyncReturns) + + const version = detectElixirVersion('/tmp/project') + + expect(version).toMatchObject({ major: 1, minor: 19, patch: 3, raw: 'Elixir 1.19.3' }) + expect(shouldRecommendElixir120(version)).toBe(true) + }) + + it('does not recommend upgrades for Elixir 1.20+', () => { + expect(shouldRecommendElixir120({ major: 1, minor: 20, patch: 0, raw: 'Elixir 1.20.0' })).toBe( + false + ) + }) }) diff --git a/packages/fixtures/demo_project/mix.exs b/packages/fixtures/demo_project/mix.exs index 02e05d56..d8996bee 100644 --- a/packages/fixtures/demo_project/mix.exs +++ b/packages/fixtures/demo_project/mix.exs @@ -5,7 +5,7 @@ defmodule PiDemoProject.MixProject do [ app: :pi_demo_project, version: "0.1.0", - elixir: "~> 1.20", + elixir: "~> 1.16", start_permanent: Mix.env() == :prod, deps: deps() ] diff --git a/packages/fixtures/demo_project/mix.lock b/packages/fixtures/demo_project/mix.lock index 8244c50d..569de43f 100644 --- a/packages/fixtures/demo_project/mix.lock +++ b/packages/fixtures/demo_project/mix.lock @@ -10,7 +10,7 @@ "finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [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", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, - "json_codec": {:hex, :json_codec, "0.1.3", "e4d8bdc8434da7f60cd519455d5b0ce32128fa05dd4049f3c1020cd0eaff4deb", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "3ed072d68de3df45e4d2220db266bf550eaee057e45e544d4ae140bf5726475e"}, + "json_codec": {:hex, :json_codec, "0.1.5", "44532ea8604f3587d0e8b0b33002745ba068c9cb1663d73c1eee49a5e7cd7e78", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6631dd3ce56a567e57ccb7a1129f57bb9206663fbb644c6ee47acdba36933f73"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.9.0", "d6f534c2a3e98b2a8cc749b4796eb77e9e3af79a76f96e4c74035a827de0d318", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "007154c7d8c43916aed3c93afd1f11aebbaa9c5ff4b7ba55ebe0d17ee0296042"},