machinaos 0.0.80 → 0.0.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { spawn, execSync } from 'child_process';
4
4
  import { dirname, resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import { readFileSync } from 'fs';
6
+ import { readFileSync, existsSync } from 'fs';
7
7
 
8
8
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
9
9
  const PKG = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf-8'));
@@ -126,7 +126,33 @@ function doctor() {
126
126
  console.log('');
127
127
  }
128
128
 
129
+ // Resolve <ROOT>/.cli-venv Python if the postinstall step provisioned it.
130
+ // Returns null on source checkouts (no venv -> fall back to ``npm run``).
131
+ function venvPython() {
132
+ const py = process.platform === 'win32'
133
+ ? resolve(ROOT, '.cli-venv', 'Scripts', 'python.exe')
134
+ : resolve(ROOT, '.cli-venv', 'bin', 'python');
135
+ return existsSync(py) ? py : null;
136
+ }
137
+
129
138
  function run(script, extraArgs = []) {
139
+ // Global-install fast path: spawn the venv's Python directly with
140
+ // ``-m cli <cmd>``. Skips the ``npm run`` shim that previously re-
141
+ // resolved the system ``python`` (which on PEP 668 systems lacks
142
+ // the CLI runtime deps -- typer/rich/anyio/psutil). The npm-run
143
+ // path stays as the source-checkout fallback (``pnpm run start``
144
+ // uses package.json scripts directly).
145
+ const venvPy = venvPython();
146
+ if (venvPy) {
147
+ const child = spawn(venvPy, ['-m', 'cli', script, ...extraArgs], {
148
+ cwd: ROOT,
149
+ stdio: 'inherit',
150
+ });
151
+ child.on('error', (e) => { console.error(`Failed: ${e.message}`); process.exit(1); });
152
+ child.on('close', (code) => process.exit(code || 0));
153
+ return;
154
+ }
155
+
130
156
  const npmArgs = ['run', script];
131
157
  if (extraArgs.length) npmArgs.push('--', ...extraArgs);
132
158
  const child = spawn(process.platform === 'win32' ? 'npm.cmd' : 'npm', npmArgs, {
package/cli/__main__.py CHANGED
@@ -1,38 +1,110 @@
1
1
  """Entry point for ``python -m cli``.
2
2
 
3
- pnpm scripts call ``python -m cli ...`` from the system Python,
4
- which only has ``uv`` -- not typer / rich / anyio / psutil. On first
5
- run we detect the missing imports, pip-install the declared runtime
6
- deps, and retry, so the user never has to ``pip install -e .`` by hand.
7
-
8
- The public CLI command is still ``machina`` (the user-facing name);
9
- ``cli`` is just the Python import path after the
10
- ``<repo>/machina/ <repo>/cli/`` source-dir rename.
3
+ The npm package ships the ``cli/`` source directory without its
4
+ runtime dependencies (``typer`` / ``rich`` / ``anyio`` / ``psutil`` /
5
+ ``platformdirs`` / ``pywin32`` on Windows). The previous bootstrap
6
+ ``pip install``ed them against ``sys.executable``, which fails on any
7
+ modern PEP 668 distro (Ubuntu 24.04+, Debian 12+, Homebrew Python,
8
+ NixOS) with ``error: externally-managed-environment``.
9
+
10
+ Fix: provision a private venv at ``<package_root>/.cli-venv`` via
11
+ ``uv`` (a hard install dependency verified by ``scripts/install.js``)
12
+ and re-exec under that venv's Python. ``uv pip install`` operates
13
+ inside the venv -- PEP 668 only governs the system interpreter, so
14
+ this is the canonical workaround documented by
15
+ https://peps.python.org/pep-0668/#guide-users-towards-virtual-environments.
16
+
17
+ ``scripts/install.js`` provisions the same venv at postinstall time,
18
+ so end users never pay the first-run latency. This module is the
19
+ fallback for the source-checkout / broken-install path.
11
20
  """
12
21
 
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ import shutil
13
26
  import subprocess
14
27
  import sys
28
+ from pathlib import Path
29
+
30
+ _ROOT = Path(__file__).resolve().parent.parent
31
+ _VENV_DIR = _ROOT / ".cli-venv"
32
+
33
+
34
+ def _venv_python() -> Path:
35
+ """Platform-specific path to the venv's Python interpreter."""
36
+ if sys.platform == "win32":
37
+ return _VENV_DIR / "Scripts" / "python.exe"
38
+ return _VENV_DIR / "bin" / "python"
15
39
 
16
- _RUNTIME_DEPS = (
17
- "typer>=0.12",
18
- "rich>=13",
19
- "anyio>=4",
20
- "psutil>=6",
21
- )
22
40
 
41
+ def _running_under_venv() -> bool:
42
+ """True if ``sys.executable`` is already the CLI venv's Python.
23
43
 
24
- def _bootstrap_deps() -> None:
25
- print("machina: installing runtime dependencies (first run)...", file=sys.stderr)
26
- subprocess.check_call(
27
- [sys.executable, "-m", "pip", "install", "--quiet", *_RUNTIME_DEPS]
28
- )
44
+ Guards against infinite re-exec loops when the venv's interpreter
45
+ itself can't import the deps (would mean ``uv pip install`` was
46
+ silently no-op'd -- a real bug worth surfacing, not retrying).
47
+ """
48
+ try:
49
+ return Path(sys.executable).resolve() == _venv_python().resolve()
50
+ except (OSError, ValueError):
51
+ return False
52
+
53
+
54
+ def _provision_venv() -> Path | None:
55
+ """Create ``<ROOT>/.cli-venv`` and install CLI deps via ``uv``.
56
+
57
+ Returns the venv's Python path on success, ``None`` if ``uv`` is
58
+ missing or the install fails. Output is inherited (not captured)
59
+ so the user sees ``uv``'s progress + any error context.
60
+ """
61
+ uv = shutil.which("uv")
62
+ if not uv:
63
+ return None
64
+ try:
65
+ if not _venv_python().exists():
66
+ print(
67
+ "machina: provisioning CLI runtime venv (first run)...",
68
+ file=sys.stderr,
69
+ )
70
+ subprocess.check_call([uv, "venv", "--quiet", str(_VENV_DIR)])
71
+ subprocess.check_call(
72
+ [
73
+ uv,
74
+ "pip",
75
+ "install",
76
+ "--quiet",
77
+ "--python",
78
+ str(_venv_python()),
79
+ "-e",
80
+ str(_ROOT),
81
+ ]
82
+ )
83
+ except subprocess.CalledProcessError:
84
+ return None
85
+ return _venv_python() if _venv_python().exists() else None
86
+
87
+
88
+ def _reexec_in_venv() -> None:
89
+ """Provision the venv if needed, then re-exec ``python -m cli`` under it."""
90
+ venv_py = _venv_python() if _venv_python().exists() else _provision_venv()
91
+ if not venv_py:
92
+ sys.stderr.write(
93
+ "machina: runtime dependencies are missing and the CLI venv\n"
94
+ " could not be provisioned. Install uv\n"
95
+ " (https://docs.astral.sh/uv/getting-started/installation/)\n"
96
+ " and re-run, or run `machina build` to regenerate the venv.\n"
97
+ )
98
+ sys.exit(1)
99
+ os.execv(str(venv_py), [str(venv_py), "-m", "cli", *sys.argv[1:]])
29
100
 
30
101
 
31
102
  try:
32
103
  from cli.cli import app
33
104
  except ImportError:
34
- _bootstrap_deps()
35
- from cli.cli import app
105
+ if _running_under_venv():
106
+ raise
107
+ _reexec_in_venv()
36
108
 
37
109
 
38
110
  if __name__ == "__main__":
package/cli/supervisor.py CHANGED
@@ -59,9 +59,19 @@ class ServiceSpec:
59
59
 
60
60
 
61
61
  def _full_env(spec_env: dict[str, str]) -> dict[str, str]:
62
- """Inherit parent env + force-color so child output stays readable."""
62
+ """Inherit parent env + force-color so child output stays readable.
63
+
64
+ ``VIRTUAL_ENV`` is stripped: ``uv run`` resolves the project venv
65
+ via the workspace ``pyproject.toml`` and warns when an inherited
66
+ ``VIRTUAL_ENV`` points elsewhere (which it does whenever the user
67
+ has the root ``.venv`` activated in their shell and the spec runs
68
+ with ``cwd=server/``). The warning has no operational effect --
69
+ uv ignores ``VIRTUAL_ENV`` unless ``--active`` is passed -- so we
70
+ drop it at the source rather than teach every reader to ignore it.
71
+ """
72
+ inherited = {k: v for k, v in os.environ.items() if k != "VIRTUAL_ENV"}
63
73
  return {
64
- **os.environ,
74
+ **inherited,
65
75
  "FORCE_COLOR": "1",
66
76
  "PYTHONUNBUFFERED": "1",
67
77
  **spec_env,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-flow-client",
3
3
  "private": true,
4
- "version": "0.0.80",
4
+ "version": "0.0.82",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "vite --host 0.0.0.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machinaos",
3
- "version": "0.0.80",
3
+ "version": "0.0.82",
4
4
  "description": "Open source workflow automation platform with AI agents, React Flow, and n8n-inspired architecture",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -53,6 +53,7 @@
53
53
  "!**/node_modules/",
54
54
  "!**/.vite/",
55
55
  ".env.template",
56
+ "pyproject.toml",
56
57
  "README.md",
57
58
  "install.sh",
58
59
  "install.ps1"
package/pyproject.toml ADDED
@@ -0,0 +1,57 @@
1
+ [project]
2
+ name = "machinaos-cli"
3
+ version = "0.1.0"
4
+ description = "MachinaOS project supervisor CLI (machina)."
5
+ requires-python = ">=3.12"
6
+ dependencies = [
7
+ "typer>=0.12",
8
+ "rich>=13",
9
+ "anyio>=4",
10
+ "psutil>=6",
11
+ # OS-native user data / cache / config / log dirs. Soft dependency
12
+ # in the CLI (``cli/platform_.py`` imports it inside a try/except
13
+ # and falls back to a stdlib implementation if the wheel isn't
14
+ # available) so ``machina clean`` / ``machina build`` still work
15
+ # immediately after a wipe, before ``pip install`` has had a
16
+ # chance to materialise it.
17
+ "platformdirs>=4.0",
18
+ # Required on Windows for the supervisor's Job Object (cli/tree.py).
19
+ # Without it, child processes survive abnormal supervisor exit
20
+ # (SIGKILL, BSOD, console close) and accumulate as orphans across
21
+ # dev restarts. Floor 308 is the first release shipping prebuilt
22
+ # wheels for Python 3.13.
23
+ "pywin32>=308; platform_system == 'Windows'",
24
+ ]
25
+
26
+ [project.scripts]
27
+ machina = "cli.cli:app"
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["cli"]
35
+
36
+ # The CLI runs under the user's global / pipx-managed Python — it is
37
+ # NOT a uv-managed project. ``managed = false`` tells ``uv`` to leave
38
+ # this project alone: ``uv sync`` at the repo root is a no-op, no
39
+ # ``<repo>/.venv/`` is created, ``python`` on PATH resolves to the
40
+ # user's global interpreter instead of being shadowed by a workspace
41
+ # venv. The server is the only uv-managed project (``server/.venv``).
42
+ [tool.uv]
43
+ managed = false
44
+
45
+ [dependency-groups]
46
+ dev = []
47
+
48
+ # ``machinaos-cli`` is a standalone Python CLI package -- installable
49
+ # via ``pipx install .`` / ``pip install .`` against any Python >=3.12
50
+ # without involving uv. It depends on nothing from ``server/``. The
51
+ # server has its own uv-managed venv at ``server/.venv``; the CLI
52
+ # invokes server-side commands by shelling out via ``cli.run.uv_run``
53
+ # (which runs ``uv run --no-sync ...`` with ``cwd=server/`` so uv
54
+ # discovers ``server/pyproject.toml`` and activates ``server/.venv``).
55
+ # Keeping the CLI independent of the uv workspace means contributors
56
+ # can install / run / debug ``machina`` on whatever interpreter is on
57
+ # PATH -- no per-checkout venv activation required.
@@ -23,12 +23,20 @@ const ROOT = resolve(__dirname, '..');
23
23
  process.env.PYTHONUTF8 = '1';
24
24
 
25
25
  function run(cmd, cwd = ROOT, timeoutMs = 300000) {
26
+ // Strip VIRTUAL_ENV from the spawned env. When the user runs
27
+ // ``npm install -g machinaos`` from a shell that has activated a
28
+ // venv (very common during dev), uv emits a noisy ``VIRTUAL_ENV
29
+ // ... does not match the project environment path`` warning per
30
+ // invocation. uv only honours VIRTUAL_ENV with ``--active``, which
31
+ // we never pass, so dropping it at the source is the documented
32
+ // workaround. Same fix applied to cli/supervisor.py's _full_env.
33
+ const { VIRTUAL_ENV, ...cleanEnv } = process.env;
26
34
  execSync(cmd, {
27
35
  cwd,
28
36
  stdio: 'inherit',
29
37
  shell: true,
30
38
  timeout: timeoutMs,
31
- env: { ...process.env, MACHINAOS_INSTALLING: 'true' }
39
+ env: { ...cleanEnv, MACHINAOS_INSTALLING: 'true' }
32
40
  });
33
41
  }
34
42
 
@@ -148,7 +156,7 @@ try {
148
156
  // Calculate total steps
149
157
  let totalSteps = 1; // .env always
150
158
  if (!clientDistExists) totalSteps += 2; // client deps + build
151
- totalSteps += 2; // Python deps + bytecode compile
159
+ totalSteps += 3; // Python deps + bytecode compile + CLI venv
152
160
  let step = 0;
153
161
 
154
162
  // Create .env if needed
@@ -202,6 +210,24 @@ try {
202
210
  console.log(` Warning: bytecode compilation failed (non-fatal): ${err.message}`);
203
211
  }
204
212
 
213
+ // Provision a private venv for the CLI runtime deps (typer, rich,
214
+ // anyio, psutil, platformdirs, pywin32-on-Windows). ``python -m cli``
215
+ // re-execs itself under this venv (see ``cli/__main__.py``), so the
216
+ // system Python never needs the deps -- avoids the PEP 668
217
+ // ``externally-managed-environment`` failure on Ubuntu 24.04+,
218
+ // Debian 12+, Homebrew Python, NixOS, etc.
219
+ // (https://peps.python.org/pep-0668/)
220
+ step++;
221
+ console.log(`[${step}/${totalSteps}] Provisioning CLI runtime venv...`);
222
+ const cliVenvDir = resolve(ROOT, '.cli-venv');
223
+ const cliVenvPython = process.platform === 'win32'
224
+ ? resolve(cliVenvDir, 'Scripts', 'python.exe')
225
+ : resolve(cliVenvDir, 'bin', 'python');
226
+ if (!existsSync(cliVenvPython)) {
227
+ run(`uv venv "${cliVenvDir}"`, ROOT);
228
+ }
229
+ run(`uv pip install --python "${cliVenvPython}" --quiet -e .`, ROOT);
230
+
205
231
  // WhatsApp RPC is now an npm dependency - binary downloaded via postinstall
206
232
  console.log('');
207
233
  console.log('Done!');