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
12 changes: 6 additions & 6 deletions loopx/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
)
from .global_registry import sync_project_registry_to_global
from .install_contract import (
ARCHIVE_FALLBACK_INSTALL_COMMAND,
DEFAULT_INSTALL_REPAIR_COMMAND,
archive_fallback_install_command,
install_repair_command,
)
from .orchestration import (
DEFAULT_ORCHESTRATION_MODE,
Expand Down Expand Up @@ -504,8 +504,8 @@ def bootstrap_project(
"Fix global registry write access, then rerun this command.",
"Use --no-global-sync only for an explicit local-only setup.",
],
"install_repair_command": DEFAULT_INSTALL_REPAIR_COMMAND,
"archive_fallback_install_command": ARCHIVE_FALLBACK_INSTALL_COMMAND,
"install_repair_command": install_repair_command(),
"archive_fallback_install_command": archive_fallback_install_command(),
"install_repair_note": (
"If this local LoopX install is missing or stale, repair the PyPI distribution "
"and packaged workflow skills, then confirm with loopx doctor before continuing."
Expand Down Expand Up @@ -665,8 +665,8 @@ def bootstrap_project(
f"loopx --registry {runtime_root / 'registry.global.json'} status",
f"loopx --registry {relative_state_file(project, registry_path)} history --goal-id {goal_id}",
],
"install_repair_command": DEFAULT_INSTALL_REPAIR_COMMAND,
"archive_fallback_install_command": ARCHIVE_FALLBACK_INSTALL_COMMAND,
"install_repair_command": install_repair_command(),
"archive_fallback_install_command": archive_fallback_install_command(),
"install_repair_note": (
"If this local LoopX install is missing or stale, repair the PyPI distribution "
"and packaged workflow skills, then confirm with loopx doctor before continuing."
Expand Down
62 changes: 5 additions & 57 deletions loopx/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@
PROMOTION_READINESS_RUNTIME_INDEX,
)
from .control_plane.runtime.time import chronology_key
from .install_contract import NO_CLONE_INSTALL_URL
from .install_contract import (
NO_CLONE_INSTALL_URL,
local_install_command,
no_clone_upgrade_command,
)
from .paths import configured_runtime_route, default_runtime_route, global_registry_path
from .python_install_owner import PythonInstallOwner, python_distribution_upgrade_command, resolve_python_install_owner
from .capabilities.project_skill_delivery import discover_project_scoped_skill_ids
Expand Down Expand Up @@ -81,62 +85,6 @@
}


def _powershell_literal(value: str | Path) -> str:
return "'" + str(value).replace("'", "''") + "'"


def local_install_command(repo_root: Path, *, skip_skills: bool = False) -> str:
if os.name == "nt":
return (
"pwsh -NoLogo -NoProfile -File "
f"{_powershell_literal(repo_root / 'scripts' / 'install-windows.ps1')} "
f"-Python {_powershell_literal(sys.executable)}"
+ (" -SkipSkills" if skip_skills else "")
)
command = str(repo_root / "scripts" / "install-local.sh")
return f"LOOPX_INSTALL_SKILL=0 {command}" if skip_skills else command


def no_clone_upgrade_command(
source_ref: Any = None,
*,
doctor_agent_type: str | None = None,
skip_skills: bool = False,
) -> str:
if os.name == "nt":
repo_root = Path(__file__).resolve().parents[1]
doctor_agent_arg = (
f" --agent-type {_powershell_literal(doctor_agent_type)}"
if doctor_agent_type
else ""
)
return (
f"{local_install_command(repo_root, skip_skills=skip_skills)}\n"
f"loopx doctor{doctor_agent_arg}"
)
ref = str(source_ref or "").strip()
installer = f"curl -fsSL {NO_CLONE_INSTALL_URL}"
doctor_agent_arg = (
f" --agent-type {shlex.quote(doctor_agent_type)}"
if doctor_agent_type
else ""
)
install_env: list[str] = []
if ref and ref != "stable":
install_env.append(f"LOOPX_REF={shlex.quote(ref)}")
if skip_skills:
install_env.append("LOOPX_INSTALL_SKILL=0")
if install_env:
installer = f"{installer} | env {' '.join(install_env)} bash"
else:
installer = f"{installer} | bash"
return (
f"{installer}\n"
'export PATH="$HOME/.local/bin:$PATH"\n'
f"loopx doctor{doctor_agent_arg}"
)


def user_local_bin() -> Path:
return Path.home() / ".local" / "bin"

Expand Down
125 changes: 125 additions & 0 deletions loopx/install_contract.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
"""Install, repair, and fallback command text printed to LoopX operators.
One platform-aware owner keeps the bootstrap/connect hint, the doctor freshness
command, and the archive fallback consistent. POSIX keeps the documented public
defaults; native Windows has neither a ``python3`` launcher nor Bash, so it
names the interpreter that runs here and prints the snapshot installer only for a
checkout that actually ships it.
"""

from __future__ import annotations

import os
import shlex
import sys
from pathlib import Path
from typing import Any


NO_CLONE_INSTALL_URL = "https://loopx-project.github.io/loopx/install.sh"

DEFAULT_INSTALL_COMMAND = "python3 -m pip install --upgrade loopx"
Expand All @@ -12,3 +30,110 @@
'export PATH="$HOME/.local/bin:$PATH"\n'
"loopx doctor"
)


def _powershell_literal(value: str | Path) -> str:
return "'" + str(value).replace("'", "''") + "'"


def _is_windows_host() -> bool:
"""Whether printed commands target PowerShell instead of a POSIX shell."""

return os.name == "nt"


def _checkout_snapshot_installer(repo_root: Path) -> Path | None:
"""The snapshot installer only when this module runs from the checkout that owns it.
An installed distribution places this module under site-packages, which does not
ship ``scripts/install-windows.ps1``. Printing that path would hand the operator a
``pwsh -File`` command PowerShell rejects, so the project marker and the script
itself decide whether the checkout route is real.
"""

installer = repo_root / "scripts" / "install-windows.ps1"
if installer.is_file() and (repo_root / "pyproject.toml").is_file():
return installer
return None


def _windows_pypi_upgrade_command(*, doctor_agent_arg: str, skip_skills: bool) -> str:
"""No-clone upgrade through the interpreter that owns this installation."""

lines = [f"& {_powershell_literal(sys.executable)} -m pip install --upgrade loopx"]
if not skip_skills:
lines.append(DEFAULT_WORKFLOW_SKILL_INSTALL_COMMAND)
lines.append(f"loopx doctor{doctor_agent_arg}")
return "\n".join(lines)


def local_install_command(repo_root: Path, *, skip_skills: bool = False) -> str:
if _is_windows_host():
return (
"pwsh -NoLogo -NoProfile -File "
f"{_powershell_literal(repo_root / 'scripts' / 'install-windows.ps1')} "
f"-Python {_powershell_literal(sys.executable)}"
+ (" -SkipSkills" if skip_skills else "")
)
command = str(repo_root / "scripts" / "install-local.sh")
return f"LOOPX_INSTALL_SKILL=0 {command}" if skip_skills else command


def no_clone_upgrade_command(
source_ref: Any = None,
*,
doctor_agent_type: str | None = None,
skip_skills: bool = False,
) -> str:
if _is_windows_host():
repo_root = Path(__file__).resolve().parents[1]
doctor_agent_arg = (
f" --agent-type {_powershell_literal(doctor_agent_type)}"
if doctor_agent_type
else ""
)
if _checkout_snapshot_installer(repo_root) is not None:
return (
f"{local_install_command(repo_root, skip_skills=skip_skills)}\n"
f"loopx doctor{doctor_agent_arg}"
)
return _windows_pypi_upgrade_command(
doctor_agent_arg=doctor_agent_arg, skip_skills=skip_skills
)
ref = str(source_ref or "").strip()
installer = f"curl -fsSL {NO_CLONE_INSTALL_URL}"
doctor_agent_arg = (
f" --agent-type {shlex.quote(doctor_agent_type)}"
if doctor_agent_type
else ""
)
install_env: list[str] = []
if ref and ref != "stable":
install_env.append(f"LOOPX_REF={shlex.quote(ref)}")
if skip_skills:
install_env.append("LOOPX_INSTALL_SKILL=0")
if install_env:
installer = f"{installer} | env {' '.join(install_env)} bash"
else:
installer = f"{installer} | bash"
return (
f"{installer}\n"
'export PATH="$HOME/.local/bin:$PATH"\n'
f"loopx doctor{doctor_agent_arg}"
)


def install_repair_command() -> str:
"""PyPI repair guidance that runs as printed on this host."""

if _is_windows_host():
return _windows_pypi_upgrade_command(doctor_agent_arg="", skip_skills=False)
return DEFAULT_INSTALL_REPAIR_COMMAND


def archive_fallback_install_command() -> str:
"""Archive fallback guidance that runs as printed on this host."""

if _is_windows_host():
return no_clone_upgrade_command()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Qualify the Windows fallback against the installed layout

From a clean wheel installation, real connect output reaches this new Windows branch and delegates to no_clone_upgrade_command, which derives /scripts/install-windows.ps1 from its module path. The wheel contains no such script, so real PowerShell 7.6.6 rejects the printed -File argument and exits 1 before any installer runs. All 10 new source tests pass because their checkout contains the script and parser acceptance does not prove file availability. The existing doctor helper has this source-only assumption already, but this PR newly exposes it through ordinary installed connect guidance. Detect the actual installation/checkout owner and provide reachable Windows recovery steps when the script is absent; add an installed-wheel real-entrypoint test instead of only checking that POSIX tokens are absent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已在 head 6122@8ea7c4d36455acdc91ce0e5509189986415eb7a6 修复(rebase 到 main @ 5bf53ec6e;git range-diff 确认前三笔提交内容未变)。

同一个 owner 现在区分 checkout 可用性与安装归属:_checkout_snapshot_installer 仅在运行模块所属的 LoopX checkout 同时具备 pyproject.toml 与 scripts/install-windows.ps1 时返回该脚本;no_clone_upgrade_command 否则改走 & '<interpreter>' -m pip install --upgrade loopx + 打包技能安装 + loopx doctor,并保留 doctor_agent_type、skip_skills 输入。local_install_command 与 POSIX 分支未改。

原生 Windows 证据(本 head):从本 head 构建 wheel 并隔离安装后,真实 connect --dry-run --no-global-sync 的两个块都打印 pip 恢复命令,不再出现 site-packages 下不存在的 -File 路径;该 fallback 经真实 PowerShell 7.6.5 解析 0 错误,其解释器行真实启动并打印见证标记;同一 wheel 中 loopx doctor 的 no_clone_upgrade_command 也返回同一 pip 恢复命令。source checkout 仍打印本 checkout 中真实存在的 scripts/install-windows.ps1。测试 tests/test_install_hint_platform.py 21 passed(checkout / site-packages 两套真实临时布局、安装布局子进程装载、真实解析器与解释器见证);相邻 install/doctor 套件 70 passed;ruff 通过;mypy 仅剩基线既有的 Windows-only WNOHANG 错误。

独立归属的 python_install_owner.python_distribution_upgrade_command pip 引用未改动,仍是相邻缺口。

return ARCHIVE_FALLBACK_INSTALL_COMMAND
Loading
Loading