From bff2e2a950dea9394469fe3b2418426b57bab744 Mon Sep 17 00:00:00 2001 From: Gilbert Date: Mon, 15 Jun 2026 23:37:32 +0800 Subject: [PATCH 1/4] Loosen pi_bridge elixir requirement to ~> 1.16 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pi_bridge Mix project pinned elixir: "~> 1.20", which is too strict for legacy projects still on Elixir 1.16–1.19. Loosen the requirement to "~> 1.16" in the bridge and the bundled demo_project fixture so adoption on older Elixir releases is at least allowed by mix.exs. Note: the runtime json_codec dep (hex: ~> 0.1.3) currently pins "~> 1.20" upstream, so installing pi_bridge on 1.16–1.19 still fails at deps resolution until a json_codec release is published with the same loosened requirement. Tracked separately. --- CHANGELOG.md | 4 ++++ packages/bridge/mix.exs | 2 +- packages/fixtures/demo_project/mix.exs | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32b497d9..8a389bd6 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. Note: the runtime `json_codec` dep still pins `~> 1.20` upstream; installation on 1.16–1.19 requires using a `json_codec` fork with the same loosened requirement. + ## 0.6.21 - 2026-06-14 ### Changed diff --git a/packages/bridge/mix.exs b/packages/bridge/mix.exs index a29a7e0b..3c400b3c 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(), 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() ] From 39dca8c724c98bc84847d9e460b31c4fa75ce865 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 15 Jun 2026 17:57:06 +0200 Subject: [PATCH 2/4] Recommend Elixir 1.20 for new projects --- CHANGELOG.md | 2 +- README.md | 2 + .../skills/elixir/new-project/SKILL.md | 2 + packages/extension/src/diagnostics/doctor.ts | 22 +++++ .../extension/test/diagnostics/doctor.test.ts | 86 +++++++++++++++++++ 5 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 packages/extension/test/diagnostics/doctor.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a389bd6..0373f856 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### 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. Note: the runtime `json_codec` dep still pins `~> 1.20` upstream; installation on 1.16–1.19 requires using a `json_codec` fork with the same loosened requirement. +- 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 diagnostics still recommend Elixir 1.20+ / OTP 27+ for the compiler's set-theoretic type-system improvements. Note: the runtime `json_codec` dep still pins `~> 1.20` upstream; installation on 1.16–1.19 requires using a `json_codec` fork with the same loosened requirement. ## 0.6.21 - 2026-06-14 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/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/diagnostics/doctor.ts b/packages/extension/src/diagnostics/doctor.ts index 422dad85..379881fd 100644 --- a/packages/extension/src/diagnostics/doctor.ts +++ b/packages/extension/src/diagnostics/doctor.ts @@ -48,6 +48,24 @@ function elixirVersion(cwd: string): string { return `${elixir} · ${otp}` } +function elixirUpgradeRecommendation(cwd: string): string | null { + const result = run('elixir', ['--version'], cwd) + if (!result.ok) return null + + const elixir = result.output + .split('\n') + .map((line) => line.trim()) + .find((line) => line.startsWith('Elixir ')) + const match = elixir?.match(/^Elixir\s+(\d+)\.(\d+)/) + if (!match) return null + + const major = Number(match[1]) + const minor = Number(match[2]) + if (major > 1 || (major === 1 && minor >= 20)) return null + + return 'recommendation: Elixir 1.20+ (OTP 27+) is recommended for new projects to get the compiler set-theoretic type system and improved type inference; pi_bridge still supports Elixir 1.16+ for existing legacy projects.' +} + function mixVersion(cwd: string): string { const result = run('mix', ['--version'], cwd) if (!result.ok) return 'unavailable' @@ -107,6 +125,7 @@ export function buildElixirStatusReport(cwd: string): string { const runtimeProblem = elixirRuntimeProblem() const bridgeVersion = bridgeInfo?.version ?? 'unknown' const source = connectionKind ?? (beamCwd ? 'not connected' : 'no Mix project') + const upgradeRecommendation = elixirUpgradeRecommendation(beamCwd ?? cwd) const lines = [ 'pi-elixir status', '', @@ -116,6 +135,7 @@ export function buildElixirStatusReport(cwd: string): string { `Bundled fallback: ${bundledBridgeStatus()}`, ...(runtimeProblem ? [`Runtime problem: ${runtimeProblem}`] : []), ...(unavailable ? [`Unavailable: ${unavailable}`] : []), + ...(upgradeRecommendation ? [upgradeRecommendation] : []), '', `Next: ${nextStep({ beamCwd, runtimeProblem, unavailable, connectionKind })}` ] @@ -129,6 +149,7 @@ export function buildElixirDoctorReport(cwd: string): string { const bridgeInfo = beamCwd ? getBridgeInfo(beamCwd) : undefined const unavailable = beamCwd ? getUnavailableReason(beamCwd) : undefined const incompatible = beamCwd ? getIncompatibleDependency(beamCwd) : undefined + const upgradeRecommendation = elixirUpgradeRecommendation(beamCwd ?? cwd) const mise = miseHint(beamCwd ?? cwd) const lines = [ 'pi-elixir doctor', @@ -141,6 +162,7 @@ export function buildElixirDoctorReport(cwd: string): string { `Elixir: ${elixirVersion(beamCwd ?? cwd)}`, `Mix: ${mixVersion(beamCwd ?? cwd)}`, ...(runtimeProblem ? [`runtime problem: ${runtimeProblem}`] : []), + ...(upgradeRecommendation ? [upgradeRecommendation] : []), ...(mise ? [mise] : []), ...pathWarnings(), '', diff --git a/packages/extension/test/diagnostics/doctor.test.ts b/packages/extension/test/diagnostics/doctor.test.ts new file mode 100644 index 00000000..9403e919 --- /dev/null +++ b/packages/extension/test/diagnostics/doctor.test.ts @@ -0,0 +1,86 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +vi.mock('node:child_process', () => ({ + spawnSync: vi.fn() +})) + +vi.mock('#src/connection/resolver.ts', () => ({ + getConnectionKind: vi.fn(() => null) +})) + +vi.mock('#src/connection/status.ts', () => ({ + getIncompatibleDependency: vi.fn(() => undefined), + getUnavailableReason: vi.fn(() => undefined) +})) + +vi.mock('#src/embedded/stdio-process.ts', () => ({ + getBridgeInfo: vi.fn(() => undefined), + getEmbeddedUrl: vi.fn(() => 'stdio:/tmp/project') +})) + +import * as childProcess from 'node:child_process' +import * as fs from 'node:fs' +import * as os from 'node:os' +import * as path from 'node:path' + +import { buildElixirDoctorReport, buildElixirStatusReport } from '#src/diagnostics/doctor.ts' + +function makeProject(): string { + const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-elixir-doctor-')) + fs.writeFileSync(path.join(cwd, 'mix.exs'), 'defmodule Demo.MixProject do\nend\n') + return cwd +} + +function mockVersions(elixirVersion: string) { + vi.mocked(childProcess.spawnSync).mockImplementation((command, args) => { + const executable = String(command) + const argv = Array.isArray(args) ? args.map(String) : [] + + if (executable === 'elixir' && argv.includes('--version')) { + return { + status: 0, + stdout: `Erlang/OTP 27 [erts-15.0]\nElixir ${elixirVersion}\n`, + stderr: '' + } as childProcess.SpawnSyncReturns + } + + if (executable === 'mix' && argv.includes('--version')) { + return { + status: 0, + stdout: `Erlang/OTP 27 [erts-15.0]\nMix 1.20.0\n`, + stderr: '' + } as childProcess.SpawnSyncReturns + } + + if (executable === 'mise') { + return { status: 1, stdout: '', stderr: '' } as childProcess.SpawnSyncReturns + } + + return { status: 0, stdout: '', stderr: '' } as childProcess.SpawnSyncReturns + }) +} + +describe('pi-elixir diagnostics', () => { + afterEach(() => { + vi.clearAllMocks() + }) + + it('recommends Elixir 1.20+ for projects running an older supported compiler', () => { + const cwd = makeProject() + mockVersions('1.19.3') + + expect(buildElixirDoctorReport(cwd)).toContain('Elixir 1.20+ (OTP 27+) is recommended') + expect(buildElixirStatusReport(cwd)).toContain('Elixir 1.20+ (OTP 27+) is recommended') + + fs.rmSync(cwd, { recursive: true, force: true }) + }) + + it('does not show the upgrade recommendation on Elixir 1.20+', () => { + const cwd = makeProject() + mockVersions('1.20.0') + + expect(buildElixirDoctorReport(cwd)).not.toContain('Elixir 1.20+ (OTP 27+) is recommended') + + fs.rmSync(cwd, { recursive: true, force: true }) + }) +}) From d7bd44edf56d976f25de5537ad73ddb45d45700e Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 15 Jun 2026 18:00:54 +0200 Subject: [PATCH 3/4] Move Elixir 1.20 recommendation to startup --- CHANGELOG.md | 2 +- packages/extension/src/bridge/startup-info.ts | 13 ++- packages/extension/src/diagnostics/doctor.ts | 22 ----- packages/extension/src/index.ts | 2 +- packages/extension/src/mix/runtime.ts | 39 +++++++++ .../extension/test/diagnostics/doctor.test.ts | 86 ------------------- packages/extension/test/mix/runtime.test.ts | 25 +++++- 7 files changed, 77 insertions(+), 112 deletions(-) delete mode 100644 packages/extension/test/diagnostics/doctor.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0373f856..72a3f60b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### 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 diagnostics still recommend Elixir 1.20+ / OTP 27+ for the compiler's set-theoretic type-system improvements. Note: the runtime `json_codec` dep still pins `~> 1.20` upstream; installation on 1.16–1.19 requires using a `json_codec` fork with the same loosened requirement. +- 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. Note: the runtime `json_codec` dep still pins `~> 1.20` upstream; installation on 1.16–1.19 requires using a `json_codec` fork with the same loosened requirement. ## 0.6.21 - 2026-06-14 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/diagnostics/doctor.ts b/packages/extension/src/diagnostics/doctor.ts index 379881fd..422dad85 100644 --- a/packages/extension/src/diagnostics/doctor.ts +++ b/packages/extension/src/diagnostics/doctor.ts @@ -48,24 +48,6 @@ function elixirVersion(cwd: string): string { return `${elixir} · ${otp}` } -function elixirUpgradeRecommendation(cwd: string): string | null { - const result = run('elixir', ['--version'], cwd) - if (!result.ok) return null - - const elixir = result.output - .split('\n') - .map((line) => line.trim()) - .find((line) => line.startsWith('Elixir ')) - const match = elixir?.match(/^Elixir\s+(\d+)\.(\d+)/) - if (!match) return null - - const major = Number(match[1]) - const minor = Number(match[2]) - if (major > 1 || (major === 1 && minor >= 20)) return null - - return 'recommendation: Elixir 1.20+ (OTP 27+) is recommended for new projects to get the compiler set-theoretic type system and improved type inference; pi_bridge still supports Elixir 1.16+ for existing legacy projects.' -} - function mixVersion(cwd: string): string { const result = run('mix', ['--version'], cwd) if (!result.ok) return 'unavailable' @@ -125,7 +107,6 @@ export function buildElixirStatusReport(cwd: string): string { const runtimeProblem = elixirRuntimeProblem() const bridgeVersion = bridgeInfo?.version ?? 'unknown' const source = connectionKind ?? (beamCwd ? 'not connected' : 'no Mix project') - const upgradeRecommendation = elixirUpgradeRecommendation(beamCwd ?? cwd) const lines = [ 'pi-elixir status', '', @@ -135,7 +116,6 @@ export function buildElixirStatusReport(cwd: string): string { `Bundled fallback: ${bundledBridgeStatus()}`, ...(runtimeProblem ? [`Runtime problem: ${runtimeProblem}`] : []), ...(unavailable ? [`Unavailable: ${unavailable}`] : []), - ...(upgradeRecommendation ? [upgradeRecommendation] : []), '', `Next: ${nextStep({ beamCwd, runtimeProblem, unavailable, connectionKind })}` ] @@ -149,7 +129,6 @@ export function buildElixirDoctorReport(cwd: string): string { const bridgeInfo = beamCwd ? getBridgeInfo(beamCwd) : undefined const unavailable = beamCwd ? getUnavailableReason(beamCwd) : undefined const incompatible = beamCwd ? getIncompatibleDependency(beamCwd) : undefined - const upgradeRecommendation = elixirUpgradeRecommendation(beamCwd ?? cwd) const mise = miseHint(beamCwd ?? cwd) const lines = [ 'pi-elixir doctor', @@ -162,7 +141,6 @@ export function buildElixirDoctorReport(cwd: string): string { `Elixir: ${elixirVersion(beamCwd ?? cwd)}`, `Mix: ${mixVersion(beamCwd ?? cwd)}`, ...(runtimeProblem ? [`runtime problem: ${runtimeProblem}`] : []), - ...(upgradeRecommendation ? [upgradeRecommendation] : []), ...(mise ? [mise] : []), ...pathWarnings(), '', 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/diagnostics/doctor.test.ts b/packages/extension/test/diagnostics/doctor.test.ts deleted file mode 100644 index 9403e919..00000000 --- a/packages/extension/test/diagnostics/doctor.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' - -vi.mock('node:child_process', () => ({ - spawnSync: vi.fn() -})) - -vi.mock('#src/connection/resolver.ts', () => ({ - getConnectionKind: vi.fn(() => null) -})) - -vi.mock('#src/connection/status.ts', () => ({ - getIncompatibleDependency: vi.fn(() => undefined), - getUnavailableReason: vi.fn(() => undefined) -})) - -vi.mock('#src/embedded/stdio-process.ts', () => ({ - getBridgeInfo: vi.fn(() => undefined), - getEmbeddedUrl: vi.fn(() => 'stdio:/tmp/project') -})) - -import * as childProcess from 'node:child_process' -import * as fs from 'node:fs' -import * as os from 'node:os' -import * as path from 'node:path' - -import { buildElixirDoctorReport, buildElixirStatusReport } from '#src/diagnostics/doctor.ts' - -function makeProject(): string { - const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-elixir-doctor-')) - fs.writeFileSync(path.join(cwd, 'mix.exs'), 'defmodule Demo.MixProject do\nend\n') - return cwd -} - -function mockVersions(elixirVersion: string) { - vi.mocked(childProcess.spawnSync).mockImplementation((command, args) => { - const executable = String(command) - const argv = Array.isArray(args) ? args.map(String) : [] - - if (executable === 'elixir' && argv.includes('--version')) { - return { - status: 0, - stdout: `Erlang/OTP 27 [erts-15.0]\nElixir ${elixirVersion}\n`, - stderr: '' - } as childProcess.SpawnSyncReturns - } - - if (executable === 'mix' && argv.includes('--version')) { - return { - status: 0, - stdout: `Erlang/OTP 27 [erts-15.0]\nMix 1.20.0\n`, - stderr: '' - } as childProcess.SpawnSyncReturns - } - - if (executable === 'mise') { - return { status: 1, stdout: '', stderr: '' } as childProcess.SpawnSyncReturns - } - - return { status: 0, stdout: '', stderr: '' } as childProcess.SpawnSyncReturns - }) -} - -describe('pi-elixir diagnostics', () => { - afterEach(() => { - vi.clearAllMocks() - }) - - it('recommends Elixir 1.20+ for projects running an older supported compiler', () => { - const cwd = makeProject() - mockVersions('1.19.3') - - expect(buildElixirDoctorReport(cwd)).toContain('Elixir 1.20+ (OTP 27+) is recommended') - expect(buildElixirStatusReport(cwd)).toContain('Elixir 1.20+ (OTP 27+) is recommended') - - fs.rmSync(cwd, { recursive: true, force: true }) - }) - - it('does not show the upgrade recommendation on Elixir 1.20+', () => { - const cwd = makeProject() - mockVersions('1.20.0') - - expect(buildElixirDoctorReport(cwd)).not.toContain('Elixir 1.20+ (OTP 27+) is recommended') - - fs.rmSync(cwd, { recursive: true, force: true }) - }) -}) 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 + ) + }) }) From 613899757d772ee0c34b019c5509b2198d5ba440 Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Mon, 15 Jun 2026 18:19:48 +0200 Subject: [PATCH 4/4] Pin json_codec with loosened Elixir requirement --- CHANGELOG.md | 2 +- packages/bridge/mix.exs | 2 +- packages/bridge/mix.lock | 2 +- packages/fixtures/demo_project/mix.lock | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72a3f60b..e74c6e22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### 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. Note: the runtime `json_codec` dep still pins `~> 1.20` upstream; installation on 1.16–1.19 requires using a `json_codec` fork with the same loosened requirement. +- 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 diff --git a/packages/bridge/mix.exs b/packages/bridge/mix.exs index 3c400b3c..8cabfdf0 100644 --- a/packages/bridge/mix.exs +++ b/packages/bridge/mix.exs @@ -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/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"},