Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/test-mac.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ jobs:
AGENTSWARM_PRODUCT_TUI_LOGO_RIGHT: '["","███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗","██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║","███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║","╚════██║██║███╗██║██╔══██║██╔══██╗██║╚██╔╝██║","███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║","╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝"]'
AGENTSWARM_PRODUCT_WORDMARK_LINES: '[""," ██████╗ ██████╗ ███████╗███╗ ██╗ ███████╗██╗ ██╗ █████╗ ██████╗ ███╗ ███╗","██╔═══██╗██╔══██╗██╔════╝████╗ ██║ ██╔════╝██║ ██║██╔══██╗██╔══██╗████╗ ████║","██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ███████╗██║ █╗ ██║███████║██████╔╝██╔████╔██║","██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ╚════██║██║███╗██║██╔══██╗██╔══██╗██║╚██╔╝██║","╚██████╔╝██║ ███████╗██║ ╚████║ ███████║╚███╔███╔╝██║ ██║██║ ██║██║ ╚═╝ ██║"," ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝"]'
AGENTSWARM_PRODUCT_PYTHON_ENVIRONMENT: standalone
AGENTSWARM_PRODUCT_VERSION: 1.0.1-rc.5
AGENTSWARM_PRODUCT_VERSION: 1.0.1-rc.6
run: |
bun install
cd packages/opencode
bun run script/build.ts --single --skip-install
test "$(./dist/agentswarm-cli-darwin-arm64/bin/agentswarm --version)" = "1.0.1-rc.5"
test "$(./dist/agentswarm-cli-darwin-arm64/bin/agentswarm --version)" = "1.0.1-rc.6"
- name: Test startup
shell: bash
run: |
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vrsen/openswarm",
"version": "1.0.1-rc.5",
"version": "1.0.1-rc.6",
"description": "An open-source multi-agent AI team built on Agency Swarm",
"license": "MIT",
"publishConfig": {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "open-swarm"
description = "An open-source multi-agent AI team built on Agency Swarm and the OpenAI Agents SDK"
version = "1.0.1-rc.5"
version = "1.0.1-rc.6"
license = { text = "MIT" }
keywords = ["agency-swarm", "openai", "multi-agent", "open-source", "openswarm"]
requires-python = ">=3.12"
Expand Down
100 changes: 75 additions & 25 deletions run_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,23 +151,84 @@ def _resolve_tui_binary(repo: Path, download: bool) -> Path | None:
return None


def _ensure_node_playwright_browsers(repo: Path) -> None:
_REQUIRED_SLIDES_NODE_PACKAGES = (
"dom-to-pptx",
"playwright",
"pptxgenjs",
"react",
"react-dom",
"react-icons",
"sharp",
)


def _warn_slides_setup(message: str) -> None:
print(
f"Warning: {message}\n"
" OpenSwarm will continue, but Slides Agent export features may be unavailable.\n"
)


def _run_optional_node_command(
cmd: list[str],
repo: Path,
label: str,
env: dict[str, str] | None = None,
) -> bool:
try:
result = subprocess.run(cmd, cwd=str(repo), env=env)
except Exception as exc:
_warn_slides_setup(f"{label} failed: {exc}")
return False
if result.returncode != 0:
_warn_slides_setup(f"{label} exited with code {result.returncode}")
return False
return True


def _ensure_node_playwright_browsers(repo: Path) -> bool:
"""Install Node Playwright browsers where the HTML-to-PPTX runner looks for them."""
cli = repo / "node_modules" / "playwright" / "cli.js"
if not cli.exists():
return
npx = shutil.which("npx")
if not npx:
_warn_slides_setup(
"npm is available but npx was not found; cannot install Node Playwright browsers"
)
return False

env = os.environ.copy()
env["PLAYWRIGHT_BROWSERS_PATH"] = str(repo / ".playwright-browsers")
subprocess.check_call(
["node", str(cli), "install", "chromium"],
cwd=str(repo),
return _run_optional_node_command(
[npx, "-y", "playwright", "install", "chromium", "chromium-headless-shell"],
repo,
"Node Playwright browser install",
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)


def _ensure_node_dependencies(repo: Path, npm: str) -> bool:
_node_modules = repo / "node_modules"
_pkg_lock = repo / "package-lock.json"
_npm_marker = _node_modules / ".package-lock.json"
_need_npm = (
not _node_modules.exists()
or not _npm_marker.exists()
or (_pkg_lock.exists() and _pkg_lock.stat().st_mtime > _npm_marker.stat().st_mtime)
or any(not (_node_modules / name).exists() for name in _REQUIRED_SLIDES_NODE_PACKAGES)
)
_npm_ok = True
if _need_npm:
print("Installing Node.js dependencies, please wait…\n")
_npm_ok = _run_optional_node_command(
[npm, "install", "--legacy-peer-deps"],
repo,
"Node.js dependency install",
)
if _npm_ok:
print("\nDone.\n")
_browsers_ok = _ensure_node_playwright_browsers(repo)
return _npm_ok and _browsers_ok


def _uv_env() -> dict[str, str]:
env = os.environ.copy()
env.setdefault("UV_LINK_MODE", "copy")
Expand Down Expand Up @@ -254,25 +315,14 @@ def _bootstrap() -> None:
" Install it from: https://poppler.freedesktop.org\n"
)

# Install Node.js dependencies if node_modules is missing or outdated.
# Install Node.js dependencies if node_modules is missing, incomplete, or outdated.
_npm = shutil.which("npm")
if _npm and (_repo / "package.json").exists():
_node_modules = _repo / "node_modules"
_pkg_lock = _repo / "package-lock.json"
_npm_marker = _node_modules / ".package-lock.json"
_need_npm = (
not _node_modules.exists()
or not _npm_marker.exists()
or (_pkg_lock.exists() and _pkg_lock.stat().st_mtime > _npm_marker.stat().st_mtime)
_ensure_node_dependencies(_repo, _npm)
elif (_repo / "package.json").exists():
_warn_slides_setup(
"npm was not found; cannot install Slides Agent Node.js dependencies"
)
if _need_npm:
print("Installing Node.js dependencies, please wait…\n")
subprocess.check_call([_npm, "install"], cwd=str(_repo))
print("\nDone.\n")
try:
_ensure_node_playwright_browsers(_repo)
except Exception:
pass

# Download the OpenSwarm TUI binary from GitHub Releases if missing.
if not os.getenv("AGENTSWARM_BIN"):
Expand Down
144 changes: 144 additions & 0 deletions scripts/smoke-bootstrap-onboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,153 @@ def smoke_onboard_env_writes() -> None:
raise RuntimeError(f"onboarding did not write expected .env values: {missing}")


def smoke_bootstrap_node_setup_installs_slides_dependencies() -> None:
sys.path.insert(0, str(ROOT))
try:
import run_utils
finally:
sys.path.pop(0)

calls: list[dict[str, object]] = []

def run(cmd: list[str], **kwargs: object) -> types.SimpleNamespace:
calls.append({"cmd": cmd, **kwargs})
if cmd == ["npm", "install", "--legacy-peer-deps"]:
modules = Path(str(kwargs["cwd"])) / "node_modules"
modules.mkdir(exist_ok=True)
(modules / ".package-lock.json").write_text("{}\n", encoding="utf-8")
for name in run_utils._REQUIRED_SLIDES_NODE_PACKAGES:
(modules / name).mkdir(parents=True, exist_ok=True)
return types.SimpleNamespace(returncode=0)
if cmd[-3:] == ["install", "chromium", "chromium-headless-shell"]:
env = kwargs.get("env")
if not isinstance(env, dict):
raise RuntimeError("Playwright install did not receive an environment")
browsers = Path(str(env["PLAYWRIGHT_BROWSERS_PATH"]))
(browsers / "chromium-1000").mkdir(parents=True)
(browsers / "chromium_headless_shell-1000").mkdir()
return types.SimpleNamespace(returncode=0)

with tempfile.TemporaryDirectory(prefix="openswarm-node-bootstrap-smoke-") as tmp:
repo = Path(tmp)
(repo / "package.json").write_text('{"dependencies":{"playwright":"^1.59.1"}}\n', encoding="utf-8")
modules = repo / "node_modules"
modules.mkdir()
(modules / ".package-lock.json").write_text("{}\n", encoding="utf-8")
present = ("dom-to-pptx", "playwright", "pptxgenjs", "react", "react-dom", "react-icons")
for name in present:
(modules / name).mkdir()

with (
patch.object(
run_utils.shutil,
"which",
lambda name: "/usr/local/bin/npx" if name == "npx" else None,
),
patch.object(run_utils.subprocess, "run", run),
):
if not run_utils._ensure_node_dependencies(repo, "npm"):
raise RuntimeError("bootstrap reported failed Node setup for successful commands")

expected = [
["npm", "install", "--legacy-peer-deps"],
[
"/usr/local/bin/npx",
"-y",
"playwright",
"install",
"chromium",
"chromium-headless-shell",
],
]
actual = [call["cmd"] for call in calls]
if actual != expected:
raise RuntimeError(f"bootstrap ran unexpected Node setup commands: {actual}")

missing = [
name
for name in run_utils._REQUIRED_SLIDES_NODE_PACKAGES
if not (repo / "node_modules" / name).exists()
]
if missing:
raise RuntimeError(f"bootstrap left required Slides npm modules missing: {missing}")

browsers = repo / ".playwright-browsers"
browser_prefixes = {path.name.split("-")[0] for path in browsers.iterdir()}
if {"chromium", "chromium_headless_shell"} - browser_prefixes:
raise RuntimeError(f"bootstrap left required Node Playwright browser assets missing: {sorted(browser_prefixes)}")

for call in calls:
if call.get("cwd") != str(repo):
raise RuntimeError(f"bootstrap ran Node setup from wrong cwd: {calls}")

env = calls[1].get("env")
if not isinstance(env, dict):
raise RuntimeError(f"bootstrap did not pass an environment to Playwright: {calls[1]}")
if env.get("PLAYWRIGHT_BROWSERS_PATH") != str(repo / ".playwright-browsers"):
raise RuntimeError(f"bootstrap set wrong Playwright browser path: {env.get('PLAYWRIGHT_BROWSERS_PATH')}")

sink = io.StringIO()
with (
patch.object(run_utils.shutil, "which", lambda _name: None),
patch("sys.stdout", sink),
):
if run_utils._ensure_node_playwright_browsers(repo):
raise RuntimeError("bootstrap reported successful Node Playwright setup without npx")
if "npx was not found" not in sink.getvalue():
raise RuntimeError("bootstrap did not warn when npx was unavailable")

def fail_run(_cmd: list[str], **_kwargs: object) -> types.SimpleNamespace:
return types.SimpleNamespace(returncode=7)

(repo / "node_modules" / "sharp").rmdir()
sink = io.StringIO()
with (
patch.object(run_utils.subprocess, "run", fail_run),
patch.object(
run_utils.shutil,
"which",
lambda name: "/usr/local/bin/npx" if name == "npx" else None,
),
patch("sys.stdout", sink),
):
if run_utils._ensure_node_dependencies(repo, "npm"):
raise RuntimeError("bootstrap reported successful Node setup after command failures")
if "OpenSwarm will continue" not in sink.getvalue():
raise RuntimeError("bootstrap did not continue visibly after failed Node setup")

invoked: list[tuple[Path, str]] = []

def which(name: str) -> str | None:
if name == "npm":
return "npm"
if name in {"soffice", "soffice.com", "pdftoppm"}:
return f"/usr/bin/{name}"
return None

replacements = {
"dotenv": module("dotenv"),
"rich": module("rich"),
"questionary": module("questionary"),
"agency_swarm": module("agency_swarm"),
}
with (
swapped_modules(replacements),
patch.object(run_utils.shutil, "which", which),
patch.object(run_utils.subprocess, "check_call", lambda *_args, **_kwargs: None),
patch.object(run_utils, "_ensure_node_dependencies", lambda repo, npm: invoked.append((repo, npm))),
patch.dict(os.environ, {"AGENTSWARM_BIN": "test-bin"}),
):
run_utils._bootstrap()

if invoked != [(ROOT, "npm")]:
raise RuntimeError(f"bootstrap did not invoke Node dependency setup for package.json: {invoked}")


def main() -> int:
smoke_swarm_import_skips_bootstrap()
smoke_onboard_env_writes()
smoke_bootstrap_node_setup_installs_slides_dependencies()
print("OpenSwarm import bootstrap and onboarding smoke passed")
return 0

Expand Down
Loading