Skip to content

Commit 4b4a3c3

Browse files
AVADSA25Mikarina13claude
authored
Find Python 3.13 instead of hard-coding /usr/local/bin (issue #424) (#441)
Five PM2 services were pinned to /usr/local/bin/python3.13, so `pm2 start ecosystem.config.js` failed with "Script not found" on Macs where Python lives elsewhere (Homebrew on Apple Silicon: /opt/homebrew/bin). ecosystem.config.js now uses CODEC_PYTHON if set, else the first python3.13 in /usr/local/bin, /opt/homebrew/bin or on PATH, else python3. Machines where /usr/local/bin/python3.13 exists resolve exactly as before. README says how to install or point at it. Co-authored-by: Mickael Farina <farina.mickael@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7a11a81 commit 4b4a3c3

3 files changed

Lines changed: 90 additions & 5 deletions

File tree

‎README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,14 @@ python3 -c "from codec_config import *; print('Config OK')"
572572
- **RGBA→JPEG crash**: macOS screenshots are PNG with alpha (RGBA). Must `img.convert("RGB")` before saving as JPEG
573573
</details>
574574

575+
<details>
576+
<summary><strong>PM2 says "Script not found: /usr/local/bin/python3.13"</strong></summary>
577+
578+
- Five services (codec-mcp-http, codec-dictate, codec-autopilot, codec-observer, codec-agent-runner) run on Python 3.13. `ecosystem.config.js` looks for `python3.13` in `/usr/local/bin`, then `/opt/homebrew/bin`, then your `PATH`, and falls back to `python3`.
579+
- Install it if it is missing: `brew install python@3.13` (or the python.org installer), then `pm2 start ecosystem.config.js` again.
580+
- Python 3.13 somewhere else? Point CODEC at it: `CODEC_PYTHON=/path/to/python3.13 pm2 start ecosystem.config.js`.
581+
</details>
582+
575583
<details>
576584
<summary><strong>Dictate not working</strong></summary>
577585

‎ecosystem.config.js‎

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,28 @@
2525
* after 30s up). The backoff starts at each app's restart_delay, so no app
2626
* restarts sooner than it did before.
2727
*/
28+
// Python for the services pinned to 3.13: CODEC_PYTHON if set, else the first python3.13
29+
// found in /usr/local/bin (python.org installer), /opt/homebrew/bin (Homebrew on Apple
30+
// Silicon) or on PATH, else plain python3. A hard-coded /usr/local/bin/python3.13 made
31+
// `pm2 start ecosystem.config.js` fail with "Script not found" on Macs where Python lives
32+
// elsewhere (GitHub issue #424).
33+
const fs = require("fs");
34+
const path = require("path");
35+
function findPython313() {
36+
if (process.env.CODEC_PYTHON) return process.env.CODEC_PYTHON;
37+
const dirs = ["/usr/local/bin", "/opt/homebrew/bin"].concat((process.env.PATH || "").split(path.delimiter));
38+
for (const dir of dirs) {
39+
if (!dir) continue;
40+
const candidate = path.join(dir, "python3.13");
41+
try {
42+
fs.accessSync(candidate, fs.constants.X_OK);
43+
return candidate;
44+
} catch (e) { /* not here */ }
45+
}
46+
return "python3";
47+
}
48+
const PYTHON313 = findPython313();
49+
2850
function withCrashCaps(app) {
2951
return {
3052
min_uptime: "60s",
@@ -62,7 +84,7 @@ module.exports = {
6284
// ── CODEC MCP HTTP bridge (remote Claude access via Cloudflare) ──
6385
{
6486
name: "codec-mcp-http",
65-
script: "/usr/local/bin/python3.13",
87+
script: PYTHON313,
6688
args: "codec_mcp_http.py",
6789
cwd: __dirname,
6890
env: {
@@ -79,7 +101,7 @@ module.exports = {
79101
// NOTE: codec-heartbeat and codec-scheduler are unified into codec-dashboard
80102
{
81103
name: "codec-dictate",
82-
script: "/usr/local/bin/python3.13",
104+
script: PYTHON313,
83105
args: "-u codec_dictate.py",
84106
cwd: __dirname,
85107
max_memory_restart: "768M",
@@ -179,7 +201,7 @@ module.exports = {
179201
// ── Autopilot (ambient scheduler — fires skills at configured times) ──
180202
{
181203
name: "codec-autopilot",
182-
script: "/usr/local/bin/python3.13",
204+
script: PYTHON313,
183205
args: "-u codec_autopilot.py",
184206
cwd: __dirname,
185207
max_memory_restart: "128M",
@@ -208,7 +230,7 @@ module.exports = {
208230
// content, no file paths leaked to ~/.codec/audit.log.
209231
{
210232
name: "codec-observer",
211-
script: "/usr/local/bin/python3.13",
233+
script: PYTHON313,
212234
args: "-u codec_observer.py",
213235
cwd: __dirname,
214236
max_memory_restart: "128M",
@@ -228,7 +250,7 @@ module.exports = {
228250
// Kill switch: AGENT_RUNNER_ENABLED=false (daemon idles).
229251
{
230252
name: "codec-agent-runner",
231-
script: "/usr/local/bin/python3.13",
253+
script: PYTHON313,
232254
args: "-u codec_agent_runner.py",
233255
cwd: __dirname,
234256
max_memory_restart: "256M",

‎tests/test_ecosystem_python.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""The PM2 ecosystem finds Python 3.13 instead of hard-coding one path (GitHub issue #424).
2+
3+
Five services were pinned to /usr/local/bin/python3.13, so `pm2 start ecosystem.config.js`
4+
failed with "Script not found" on Macs where Python lives elsewhere (Homebrew on Apple
5+
Silicon uses /opt/homebrew/bin).
6+
"""
7+
from __future__ import annotations
8+
9+
import json
10+
import os
11+
import shutil
12+
import subprocess
13+
from pathlib import Path
14+
15+
import pytest
16+
17+
REPO = Path(__file__).resolve().parent.parent
18+
ECO = REPO / "ecosystem.config.js"
19+
PINNED = ("codec-mcp-http", "codec-dictate", "codec-autopilot", "codec-observer", "codec-agent-runner")
20+
21+
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="needs node to load ecosystem.config.js")
22+
23+
24+
def _scripts(env: dict) -> dict:
25+
out = subprocess.run(
26+
["node", "-e", f"const c=require({json.dumps(str(ECO))});"
27+
"console.log(JSON.stringify(Object.fromEntries(c.apps.map(a=>[a.name,a.script]))))"],
28+
capture_output=True, text=True, env=env, timeout=30, check=True)
29+
return json.loads(out.stdout)
30+
31+
32+
def test_no_service_hard_codes_a_python_path():
33+
assert '"/usr/local/bin/python3.13"' not in ECO.read_text(encoding="utf-8").split("function findPython313")[1]
34+
35+
36+
def test_codec_python_wins():
37+
env = dict(os.environ, CODEC_PYTHON="/custom/bin/python3.13")
38+
scripts = _scripts(env)
39+
assert {scripts[n] for n in PINNED} == {"/custom/bin/python3.13"}
40+
41+
42+
def test_a_python313_on_path_is_found(tmp_path):
43+
fake = tmp_path / "python3.13"
44+
fake.write_text("#!/bin/sh\n")
45+
fake.chmod(0o755)
46+
env = {k: v for k, v in os.environ.items() if k != "CODEC_PYTHON"}
47+
env["PATH"] = f"{tmp_path}{os.pathsep}{env.get('PATH', '')}"
48+
scripts = _scripts(env)
49+
found = {scripts[n] for n in PINNED}
50+
assert len(found) == 1, "the five services use the same Python"
51+
(python,) = found
52+
# The usual install places come first; otherwise the one on PATH.
53+
assert python.endswith("/python3.13") and os.access(python, os.X_OK)
54+
if not any(os.access(f"{d}/python3.13", os.X_OK) for d in ("/usr/local/bin", "/opt/homebrew/bin")):
55+
assert python == str(fake)

0 commit comments

Comments
 (0)