← Files PixVerseARCHIVED FILE
pvx/internal_cli.py
45.1 KB · Oct 3, 2026 · 06:30 UTC
from __future__ import annotations
import hashlib
import json
import os
import re
import shutil
import stat
import subprocess
import tempfile
import unicodedata
import zipfile
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path, PurePosixPath, PureWindowsPath
from typing import Any, Callable
from .canvas_sync import (
CANVAS_OPTIONAL_REVIEWED_COMMANDS,
CANVAS_REVIEWED_COMMAND_POLICIES,
CanvasSyncLockError,
canvas_command_contract_issues,
canvas_sync_lock,
)
BUNDLED_CLI_DIRECTORY = "bundled-cli"
BUNDLED_CLI_ARCHIVE = f"{BUNDLED_CLI_DIRECTORY}/pixverse-internal.zip"
BUNDLED_CLI_MANIFEST = f"{BUNDLED_CLI_DIRECTORY}/manifest.json"
SOURCE_CLI_DIRECTORY = "internal-cli"
INTERNAL_CLI_CHANNEL = "internal"
ONLINE_CLI_CHANNEL = "online"
MAX_ARCHIVE_ENTRIES = 10_000
MAX_UNCOMPRESSED_BYTES = 512 * 1024 * 1024
INSTALL_RECORD_FILENAME = "internal-install.json"
ACTIVE_RECORD_FILENAME = "active.json"
INSTALL_LOCK_STATE_FILENAME = ".internal-cli-install"
WINDOWS_RESERVED_PATH_NAMES = {
"con",
"prn",
"aux",
"nul",
*(f"com{number}" for number in range(1, 10)),
*(f"lpt{number}" for number in range(1, 10)),
}
REQUIRED_COMMANDS = (
"pixverse capabilities",
"pixverse capabilities create",
"pixverse capabilities canvas",
"pixverse auth login",
"pixverse auth status",
"pixverse account info",
"pixverse account slots",
"pixverse account usage",
"pixverse create video",
"pixverse create image",
"pixverse create transition",
"pixverse create voice",
"pixverse create music",
"pixverse create extend",
"pixverse create modify",
"pixverse create upscale",
"pixverse create reference",
"pixverse create motion-control",
"pixverse create template",
"pixverse task status",
"pixverse asset info",
"pixverse asset download",
"pixverse asset list",
"pixverse canvas project create",
"pixverse subscribe",
)
REQUIRED_CREATE_CAPABILITY_MODES = (
"video",
"image",
"transition",
"voice",
"music",
"extend",
"modify",
"upscale",
"reference",
"motion-control",
"template",
)
CREATE_CAPABILITIES_SCHEMA = "pixverse_create_capabilities.v1"
CANVAS_BINDINGS_SCHEMA = "pixverse_canvas_cli_bindings.v1"
CANVAS_CAPABILITY_DISCOVERY = {
"merged_command": "pixverse capabilities canvas --json",
"raw_command": "pixverse capabilities canvas --raw --json",
"schema_command": "pixverse canvas node schema --node-type <type> --json",
"adapter_contract_revision": "canvas_cli_capability_adapter.v2",
}
PIXVERSE_REGION_OPTION = "--region <region>"
PIXVERSE_REGION_OPTION_DESCRIPTION = (
"Region: global or cn (default: global; PIXVERSE_REGION overrides) | choices: global, cn"
)
class InternalCliError(RuntimeError):
pass
class InternalCliInstallError(InternalCliError):
pass
@dataclass(frozen=True)
class InternalCliArtifact:
path: Path
archive_sha256: str
archive_root: str
package_name: str
version: str
core_version: str
node_requirement: str
capabilities_schema: str
capabilities_sha256: str
command_count: int
commands: tuple[str, ...]
has_lockfile: bool
def bundle_manifest(self) -> dict[str, Any]:
return {
"schema_version": 1,
"channel": INTERNAL_CLI_CHANNEL,
"distribution": "standalone-zip",
"source_filename": self.path.name,
"package": self.package_name,
"version": self.version,
"core_version": self.core_version,
"node": self.node_requirement,
"artifact": "pixverse-internal.zip",
"artifact_sha256": self.archive_sha256,
"entrypoints": {
"posix": "pixverse",
"windows": "pixverse.cmd",
"node": "dist/index.js",
},
"capabilities": {
"path": "dist/capabilities.json",
"schema_version": self.capabilities_schema,
"sha256": self.capabilities_sha256,
"command_count": self.command_count,
},
"dependency_install": "npm-ci" if self.has_lockfile else "npm-install",
}
def file_sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def bytes_sha256(value: bytes) -> str:
return hashlib.sha256(value).hexdigest()
def normalize_version(value: str) -> str:
match = re.search(r"(?<!\d)(\d+\.\d+\.\d+)(?!\d)", value)
return match.group(1) if match else ""
def semver_tuple(value: str) -> tuple[int, int, int] | None:
normalized = normalize_version(value)
if not normalized:
return None
return tuple(int(part) for part in normalized.split(".")) # type: ignore[return-value]
def version_at_least(value: str, minimum: str) -> bool:
actual = semver_tuple(value)
floor = semver_tuple(minimum)
return bool(actual is not None and floor is not None and actual >= floor)
def node_version_satisfies(actual: str, requirement: str) -> bool:
match = re.fullmatch(r"\s*>=\s*(\d+\.\d+\.\d+)\s*", requirement)
return bool(match and version_at_least(actual, match.group(1)))
def _platform_archive_path_key(name: str) -> str:
parts = PurePosixPath(name.rstrip("/")).parts
normalized_parts: list[str] = []
for part in parts:
normalized = unicodedata.normalize("NFKC", part)
windows_normalized = normalized.rstrip(" .")
if not windows_normalized or windows_normalized != normalized:
raise InternalCliError(f"Windows-unsafe path in internal CLI ZIP: {name!r}")
folded = windows_normalized.casefold()
if folded.split(".", 1)[0] in WINDOWS_RESERVED_PATH_NAMES:
raise InternalCliError(f"Windows-reserved path in internal CLI ZIP: {name!r}")
normalized_parts.append(folded)
return "/".join(normalized_parts)
def _runtime_executable(destination: Path) -> Path:
return destination / ("pixverse.cmd" if os.name == "nt" else "pixverse")
def _runtime_executable_is_usable(destination: Path) -> bool:
executable = _runtime_executable(destination)
return executable.is_file() and (os.name == "nt" or os.access(executable, os.X_OK))
def _validate_seedance_25_mode(mode: str, payload: dict[str, Any]) -> None:
model_ids = payload.get("model_ids")
if not isinstance(model_ids, list) or "seedance-2.5" not in model_ids:
raise InternalCliError(
f"internal CLI create capability {mode!r} must include seedance-2.5"
)
model_parameters = payload.get("model_parameters")
if not isinstance(model_parameters, dict):
raise InternalCliError(
f"internal CLI create capability {mode!r} lacks model_parameters"
)
def parameter(name: str) -> dict[str, Any]:
values = model_parameters.get(name)
value = values.get("seedance-2.5") if isinstance(values, dict) else None
if not isinstance(value, dict):
raise InternalCliError(
f"internal CLI create capability {mode!r} lacks seedance-2.5 {name!r} metadata"
)
return value
duration_values = parameter("duration").get("enum")
if not isinstance(duration_values, list) or not {str(value) for value in range(4, 31)}.issubset(
{str(value) for value in duration_values}
):
raise InternalCliError(
f"internal CLI create capability {mode!r} has an unreviewed seedance-2.5 duration range"
)
if mode == "reference" and "auto" not in {str(value) for value in duration_values}:
raise InternalCliError(
"internal CLI Seedance 2.5 reference capability must expose automatic duration"
)
quality_values = parameter("quality").get("enum")
if not isinstance(quality_values, list) or set(quality_values) != {"480p", "720p", "1080p"}:
raise InternalCliError(
f"internal CLI create capability {mode!r} has an unreviewed seedance-2.5 quality set"
)
if parameter("audio").get("supported") is not False:
raise InternalCliError(
f"internal CLI create capability {mode!r} unexpectedly enables the Seedance 2.5 audio parameter; review its serialization contract"
)
if mode in {"video", "reference"}:
aspect_values = parameter("aspect_ratio").get("enum")
expected_aspects = {"auto", "21:9", "16:9", "4:3", "1:1", "3:4", "9:16"}
if not isinstance(aspect_values, list) or set(aspect_values) != expected_aspects:
raise InternalCliError(
f"internal CLI create capability {mode!r} has an unreviewed Seedance 2.5 aspect set"
)
if mode == "video":
for name in ("multi_shot", "off_peak"):
if parameter(name).get("supported") is not False:
raise InternalCliError(
f"internal CLI Seedance 2.5 video capability unexpectedly enables {name}"
)
elif mode == "reference":
if parameter("off_peak").get("supported") is not False:
raise InternalCliError(
"internal CLI Seedance 2.5 reference capability unexpectedly enables off_peak"
)
expected_limits = {"images": 30, "videos": 10, "audios": 10}
for name, limit in expected_limits.items():
if parameter(name).get("max_count") != limit:
raise InternalCliError(
f"internal CLI Seedance 2.5 reference {name} limit is no longer {limit}"
)
task_types = parameter("task_type").get("enum")
if not isinstance(task_types, list) or set(task_types) != {
"auto",
"reference",
"edit",
"extend",
}:
raise InternalCliError(
"internal CLI Seedance 2.5 reference task-type set is unreviewed"
)
elif parameter("off_peak").get("supported") is not False:
raise InternalCliError(
"internal CLI Seedance 2.5 transition capability unexpectedly enables off_peak"
)
def _create_model_parameter(
mode: str,
payload: dict[str, Any],
model: str,
parameter: str,
) -> dict[str, Any]:
model_parameters = payload.get("model_parameters")
by_model = model_parameters.get(parameter) if isinstance(model_parameters, dict) else None
value = by_model.get(model) if isinstance(by_model, dict) else None
if not isinstance(value, dict):
raise InternalCliError(
f"internal CLI create capability {mode!r} lacks {model} {parameter!r} metadata"
)
return value
def _validate_latest_create_models(modes: dict[str, Any]) -> None:
video = modes["video"]
reference = modes["reference"]
transition = modes["transition"]
if not all(isinstance(item, dict) for item in (video, reference, transition)):
raise InternalCliError("internal CLI video/reference/transition capabilities must be objects")
video_models = video.get("model_ids")
if not isinstance(video_models, list) or not {"flux-3.0", "wan-3.0"}.issubset(video_models):
raise InternalCliError("internal CLI video capabilities must include FLUX 3 and Wan 3.0")
for mode, payload in (("reference", reference), ("transition", transition)):
model_ids = payload.get("model_ids")
if not isinstance(model_ids, list) or "wan-3.0" not in model_ids:
raise InternalCliError(f"internal CLI {mode} capabilities must include Wan 3.0")
if "flux-3.0" in model_ids:
raise InternalCliError(f"internal CLI {mode} unexpectedly enables FLUX 3")
expected_flux = {
"duration": {str(value) for value in range(5, 21)},
"quality": {"720p", "1080p"},
"aspect_ratio": {"auto", "21:9", "2:1", "16:9", "4:3", "1:1", "3:4", "9:16"},
}
for parameter, expected in expected_flux.items():
observed = _create_model_parameter("video", video, "flux-3.0", parameter).get("enum")
if not isinstance(observed, list) or {str(value) for value in observed} != expected:
raise InternalCliError(f"internal CLI FLUX 3 {parameter} values changed")
if _create_model_parameter("video", video, "flux-3.0", "audio").get("supported") is not True:
raise InternalCliError("internal CLI FLUX 3 generated audio must remain supported")
for parameter in ("multi_shot", "off_peak"):
if _create_model_parameter("video", video, "flux-3.0", parameter).get("supported") is not False:
raise InternalCliError(f"internal CLI FLUX 3 unexpectedly enables {parameter}")
for mode, payload, duration_values in (
("video", video, {str(value) for value in range(2, 31)}),
("reference", reference, {*(str(value) for value in range(2, 31)), "auto"}),
("transition", transition, {str(value) for value in range(2, 31)}),
):
observed_duration = _create_model_parameter(mode, payload, "wan-3.0", "duration").get("enum")
if not isinstance(observed_duration, list) or {str(value) for value in observed_duration} != duration_values:
raise InternalCliError(f"internal CLI Wan 3.0 {mode} duration values changed")
quality = _create_model_parameter(mode, payload, "wan-3.0", "quality").get("enum")
if not isinstance(quality, list) or set(quality) != {"480p", "720p", "1080p"}:
raise InternalCliError(f"internal CLI Wan 3.0 {mode} quality values changed")
if _create_model_parameter(mode, payload, "wan-3.0", "audio").get("supported") is not True:
raise InternalCliError(f"internal CLI Wan 3.0 {mode} generated audio must remain supported")
if _create_model_parameter(mode, payload, "wan-3.0", "off_peak").get("supported") is not False:
raise InternalCliError(f"internal CLI Wan 3.0 {mode} unexpectedly enables off_peak")
for mode, payload in (("video", video), ("reference", reference)):
aspects = _create_model_parameter(mode, payload, "wan-3.0", "aspect_ratio").get("enum")
if not isinstance(aspects, list) or set(aspects) != {
"auto", "16:9", "4:3", "1:1", "3:4", "9:16"
}:
raise InternalCliError(f"internal CLI Wan 3.0 {mode} aspect values changed")
if _create_model_parameter("video", video, "wan-3.0", "multi_shot").get("supported") is not False:
raise InternalCliError("internal CLI Wan 3.0 unexpectedly enables multi_shot")
for parameter, maximum in (("images", 10), ("videos", 5), ("audios", 5)):
if _create_model_parameter("reference", reference, "wan-3.0", parameter).get("max_count") != maximum:
raise InternalCliError(
f"internal CLI Wan 3.0 reference {parameter} limit is no longer {maximum}"
)
if _create_model_parameter("reference", reference, "wan-3.0", "task_type").get("supported") is not False:
raise InternalCliError("internal CLI Wan 3.0 unexpectedly enables task_type")
metadata = reference.get("model_metadata")
wan_metadata = metadata.get("wan-3.0") if isinstance(metadata, dict) else None
if not isinstance(wan_metadata, dict) or wan_metadata.get("max_reference_items") != 20:
raise InternalCliError("internal CLI Wan 3.0 total reference limit is no longer 20")
reference_parameters = reference.get("parameters")
prompt = reference_parameters.get("prompt") if isinstance(reference_parameters, dict) else None
if not isinstance(prompt, dict) or prompt.get("required") is not True:
raise InternalCliError("internal CLI reference prompt must remain required")
transition_rules = transition.get("rules")
prompt_rule = next(
(
rule for rule in transition_rules
if isinstance(rule, dict) and rule.get("id") == "transition-prompt-required"
),
None,
) if isinstance(transition_rules, list) else None
required_models = (
prompt_rule.get("when", {}).get("model", {}).get("in", [])
if isinstance(prompt_rule, dict)
else []
)
if not isinstance(required_models, list) or "wan-3.0" not in required_models:
raise InternalCliError("internal CLI Wan 3.0 transition prompt requirement is missing")
def _validate_internal_capability_domains(
capabilities: dict[str, Any],
command_payloads: dict[str, list[dict[str, Any]]],
) -> None:
domains = capabilities.get("capability_domains")
if not isinstance(domains, dict):
raise InternalCliError("internal CLI capabilities schema 1.2.0 lacks capability_domains")
create = domains.get("create")
if not isinstance(create, dict):
raise InternalCliError("internal CLI capabilities schema 1.2.0 lacks the create domain")
if create.get("schema_version") != CREATE_CAPABILITIES_SCHEMA:
raise InternalCliError(
f"internal CLI create capability schema must be {CREATE_CAPABILITIES_SCHEMA!r}"
)
if create.get("encoding") != "normalized":
raise InternalCliError("internal CLI create capabilities must use normalized encoding")
modes = create.get("modes")
if not isinstance(modes, dict):
raise InternalCliError("internal CLI create capabilities modes must be an object")
missing_modes = sorted(set(REQUIRED_CREATE_CAPABILITY_MODES) - set(modes))
if missing_modes:
raise InternalCliError(
"internal CLI create capabilities are missing modes: " + ", ".join(missing_modes)
)
for mode in REQUIRED_CREATE_CAPABILITY_MODES:
mode_payload = modes.get(mode)
command = f"pixverse create {mode}"
if not isinstance(mode_payload, dict) or mode_payload.get("command") != command:
raise InternalCliError(
f"internal CLI create capability {mode!r} must bind to {command!r}"
)
if command not in command_payloads:
raise InternalCliError(
f"internal CLI create capability {mode!r} does not have a command entry"
)
model_catalog = create.get("model_catalog")
required_models = {"seedance-2.5", "flux-3.0", "wan-3.0"}
if not isinstance(model_catalog, dict) or any(
not isinstance(model_catalog.get(model), str) for model in required_models
):
raise InternalCliError(
"internal CLI create model catalog lacks Seedance 2.5, FLUX 3, or Wan 3.0"
)
for mode in ("video", "reference", "transition"):
_validate_seedance_25_mode(mode, modes[mode])
_validate_latest_create_models(modes)
canvas = domains.get("canvas")
if not isinstance(canvas, dict) or canvas.get("source") != "runtime":
raise InternalCliError("internal CLI Canvas capability domain must use runtime discovery")
if canvas.get("discovery") != CANVAS_CAPABILITY_DISCOVERY:
raise InternalCliError("internal CLI Canvas capability discovery commands changed")
global_options = capabilities.get("global_options")
if not isinstance(global_options, dict) or global_options.get(
PIXVERSE_REGION_OPTION
) != PIXVERSE_REGION_OPTION_DESCRIPTION:
raise InternalCliError("internal CLI global region contract changed")
bindings = capabilities.get("bindings")
canvas_bindings = bindings.get("canvas_cli") if isinstance(bindings, dict) else None
if not isinstance(canvas_bindings, dict):
raise InternalCliError("internal CLI capabilities schema 1.2.0 lacks Canvas bindings")
if canvas_bindings.get("schema_version") != CANVAS_BINDINGS_SCHEMA:
raise InternalCliError(
f"internal CLI Canvas bindings schema must be {CANVAS_BINDINGS_SCHEMA!r}"
)
binding_items = canvas_bindings.get("items")
if not isinstance(binding_items, list):
raise InternalCliError("internal CLI Canvas bindings items must be a list")
if binding_items:
raise InternalCliError(
"internal CLI contains unreviewed Canvas CLI bindings; update the wrapper before packaging"
)
def inspect_internal_cli_zip(
path: Path,
*,
minimum_version: str,
capabilities_schema: str,
expected_node_requirement: str | None = None,
) -> InternalCliArtifact:
source = path.expanduser()
if source.is_symlink():
raise InternalCliError(f"internal CLI ZIP must not be a symlink: {source}")
resolved = source.resolve()
if not resolved.is_file():
raise InternalCliError(f"internal CLI ZIP does not exist: {resolved}")
stat_result = resolved.stat()
return _inspect_internal_cli_zip_cached(
str(resolved),
stat_result.st_mtime_ns,
stat_result.st_size,
minimum_version,
capabilities_schema,
expected_node_requirement or "",
)
@lru_cache(maxsize=16)
def _inspect_internal_cli_zip_cached(
path_text: str,
_mtime_ns: int,
_size: int,
minimum_version: str,
capabilities_schema: str,
expected_node_requirement: str,
) -> InternalCliArtifact:
path = Path(path_text)
try:
archive = zipfile.ZipFile(path)
except zipfile.BadZipFile as exc:
raise InternalCliError(f"invalid internal CLI ZIP: {exc}") from exc
with archive:
infos = archive.infolist()
if not infos:
raise InternalCliError("internal CLI ZIP is empty")
if len(infos) > MAX_ARCHIVE_ENTRIES:
raise InternalCliError(f"internal CLI ZIP has too many entries: {len(infos)}")
if sum(item.file_size for item in infos) > MAX_UNCOMPRESSED_BYTES:
raise InternalCliError("internal CLI ZIP exceeds the uncompressed size limit")
if archive.testzip() is not None:
raise InternalCliError("internal CLI ZIP failed CRC verification")
names = [item.filename for item in infos]
if len(names) != len(set(names)):
raise InternalCliError("internal CLI ZIP contains duplicate paths")
roots: set[str] = set()
normalized_paths: dict[str, str] = {}
for item in infos:
name = item.filename
pure = PurePosixPath(name)
windows = PureWindowsPath(name)
if (
not name
or "\\" in name
or pure.is_absolute()
or windows.is_absolute()
or windows.drive
or ".." in pure.parts
or any(":" in part for part in pure.parts)
):
raise InternalCliError(f"unsafe path in internal CLI ZIP: {name!r}")
normalized_path = _platform_archive_path_key(name)
previous_name = normalized_paths.get(normalized_path)
if previous_name is not None and previous_name != name:
raise InternalCliError(
"internal CLI ZIP contains platform-colliding paths: "
f"{previous_name!r} and {name!r}"
)
normalized_paths[normalized_path] = name
if pure.parts:
roots.add(pure.parts[0])
mode = (item.external_attr >> 16) & 0xFFFF
if stat.S_ISLNK(mode):
raise InternalCliError(f"symlink is not allowed in internal CLI ZIP: {name}")
if mode and not (stat.S_ISREG(mode) or stat.S_ISDIR(mode)):
raise InternalCliError(f"special file is not allowed in internal CLI ZIP: {name}")
if len(roots) != 1:
raise InternalCliError("internal CLI ZIP must contain exactly one top-level directory")
root = next(iter(roots))
required = {
f"{root}/package.json",
f"{root}/dist/index.js",
f"{root}/dist/capabilities.json",
f"{root}/pixverse",
f"{root}/pixverse.cmd",
}
missing_files = sorted(required - set(names))
if missing_files:
raise InternalCliError("internal CLI ZIP is missing: " + ", ".join(missing_files))
posix_wrapper_mode = (archive.getinfo(f"{root}/pixverse").external_attr >> 16) & 0o777
if not posix_wrapper_mode & 0o111:
raise InternalCliError("internal CLI POSIX wrapper must be executable")
package_bytes = archive.read(f"{root}/package.json")
capabilities_bytes = archive.read(f"{root}/dist/capabilities.json")
try:
package = json.loads(package_bytes)
capabilities = json.loads(capabilities_bytes)
except json.JSONDecodeError as exc:
raise InternalCliError(f"internal CLI metadata is invalid JSON: {exc}") from exc
if not isinstance(package, dict) or not isinstance(capabilities, dict):
raise InternalCliError("internal CLI metadata must contain JSON objects")
package_name = str(package.get("name") or "")
version = str(package.get("version") or "")
core_version = normalize_version(version)
node_requirement = str((package.get("engines") or {}).get("node") or "")
binary = (package.get("bin") or {}).get("pixverse")
if package_name != "pixverse-internal":
raise InternalCliError(f"unexpected internal CLI package name: {package_name!r}")
if not version or not core_version:
raise InternalCliError(f"invalid internal CLI version: {version!r}")
if not version_at_least(core_version, minimum_version):
raise InternalCliError(
f"internal CLI {version!r} is below the plugin minimum {minimum_version}"
)
if binary != "./dist/index.js":
raise InternalCliError(f"unexpected internal CLI bin entry: {binary!r}")
if not re.fullmatch(r"\s*>=\s*\d+\.\d+\.\d+\s*", node_requirement):
raise InternalCliError(f"unsupported internal CLI Node requirement: {node_requirement!r}")
if expected_node_requirement and node_requirement.strip() != expected_node_requirement.strip():
raise InternalCliError(
"internal CLI Node requirement "
f"{node_requirement!r} does not match plugin requirement {expected_node_requirement!r}"
)
observed_schema = str(capabilities.get("schema_version") or "")
if observed_schema != capabilities_schema:
raise InternalCliError(
f"internal CLI capabilities schema {observed_schema!r} does not match {capabilities_schema!r}"
)
commands_payload = capabilities.get("commands")
if not isinstance(commands_payload, list):
raise InternalCliError("internal CLI capabilities commands must be a list")
command_payloads: dict[str, list[dict[str, Any]]] = {}
for item in commands_payload:
if isinstance(item, dict) and isinstance(item.get("cmd"), str):
command_payloads.setdefault(str(item["cmd"]), []).append(item)
commands = tuple(sorted(command for command in command_payloads for _ in command_payloads[command]))
missing_commands = sorted(set(REQUIRED_COMMANDS) - set(command_payloads))
if missing_commands:
raise InternalCliError(
"internal CLI is missing required commands: " + ", ".join(missing_commands)
)
duplicate_required_commands = sorted(
command for command in REQUIRED_COMMANDS if len(command_payloads.get(command, [])) != 1
)
if duplicate_required_commands:
raise InternalCliError(
"internal CLI capabilities contain duplicate required commands: "
+ ", ".join(duplicate_required_commands)
)
non_runnable_required_commands = sorted(
command
for command in REQUIRED_COMMANDS
if command_payloads[command][0].get("run") is not True
)
if non_runnable_required_commands:
raise InternalCliError(
"internal CLI required commands are not runnable: "
+ ", ".join(non_runnable_required_commands)
)
if observed_schema == "1.2.0":
_validate_internal_capability_domains(capabilities, command_payloads)
canvas_runnable_payloads = [
item
for item in commands_payload
if isinstance(item, dict)
and item.get("run") is True
and isinstance(item.get("cmd"), str)
and (
str(item["cmd"]).startswith("pixverse canvas ")
or str(item["cmd"]) == "pixverse capabilities canvas"
)
]
canvas_runnable_commands = {str(item["cmd"]) for item in canvas_runnable_payloads}
if len(canvas_runnable_payloads) != len(canvas_runnable_commands):
raise InternalCliError("internal CLI capabilities contain duplicate runnable Canvas commands")
reviewed_canvas_commands = set(CANVAS_REVIEWED_COMMAND_POLICIES)
missing_canvas_commands = sorted(
reviewed_canvas_commands - canvas_runnable_commands - CANVAS_OPTIONAL_REVIEWED_COMMANDS
)
if missing_canvas_commands:
raise InternalCliError(
"internal CLI is missing reviewed Canvas commands: " + ", ".join(missing_canvas_commands)
)
unreviewed_canvas_commands = sorted(canvas_runnable_commands - reviewed_canvas_commands)
if unreviewed_canvas_commands:
raise InternalCliError(
"internal CLI contains unreviewed runnable Canvas commands; update the plugin's Canvas "
"read/mutation policy before packaging: " + ", ".join(unreviewed_canvas_commands)
)
canvas_contract_issues = [
issue
for item in canvas_runnable_payloads
for issue in canvas_command_contract_issues(item)
]
if canvas_contract_issues:
raise InternalCliError(
"internal CLI Canvas capability contract changed; review wrapper policy before packaging: "
+ "; ".join(canvas_contract_issues)
)
capabilities_cli = capabilities.get("cli")
capabilities_version = (
str(capabilities_cli.get("version") or "") if isinstance(capabilities_cli, dict) else ""
)
if capabilities_version and normalize_version(capabilities_version) != core_version:
raise InternalCliError(
f"capabilities CLI version {capabilities_version!r} does not match package {version!r}"
)
return InternalCliArtifact(
path=path,
archive_sha256=file_sha256(path),
archive_root=root,
package_name=package_name,
version=version,
core_version=core_version,
node_requirement=node_requirement.strip(),
capabilities_schema=observed_schema,
capabilities_sha256=bytes_sha256(capabilities_bytes),
command_count=len(commands_payload),
commands=commands,
has_lockfile=f"{root}/package-lock.json" in names,
)
def discover_internal_cli_zip(root: Path, explicit: Path | None = None) -> Path:
if explicit is not None:
return _absolute_without_resolving(explicit)
configured = os.environ.get("PIXVERSE_INTERNAL_CLI_ZIP", "").strip()
if configured:
return _absolute_without_resolving(Path(configured))
directory = root / SOURCE_CLI_DIRECTORY
candidates = sorted(directory.glob("*.zip")) if directory.is_dir() else []
if not candidates:
raise InternalCliError(
f"no internal CLI ZIP found in {directory}; pass --internal-cli-zip or set PIXVERSE_INTERNAL_CLI_ZIP"
)
if len(candidates) > 1:
names = ", ".join(path.name for path in candidates)
raise InternalCliError(
f"multiple internal CLI ZIPs found in {directory}: {names}; select one explicitly"
)
return _absolute_without_resolving(candidates[0])
def _absolute_without_resolving(path: Path) -> Path:
expanded = path.expanduser()
return expanded if expanded.is_absolute() else (Path.cwd() / expanded).absolute()
def internal_cli_source_present(root: Path) -> bool:
if (root / BUNDLED_CLI_MANIFEST).is_file():
return True
directory = root / SOURCE_CLI_DIRECTORY
return directory.is_dir() and any(directory.glob("*.zip"))
def load_internal_cli_artifact(
root: Path,
*,
minimum_version: str,
capabilities_schema: str,
expected_node_requirement: str | None = None,
) -> InternalCliArtifact:
manifest_path = root / BUNDLED_CLI_MANIFEST
if manifest_path.is_file():
try:
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise InternalCliError(f"invalid bundled internal CLI manifest: {exc}") from exc
if not isinstance(manifest, dict) or manifest.get("channel") != INTERNAL_CLI_CHANNEL:
raise InternalCliError("bundled internal CLI manifest does not declare the internal channel")
if manifest.get("schema_version") != 1 or manifest.get("distribution") != "standalone-zip":
raise InternalCliError("bundled internal CLI manifest has an unsupported format")
artifact_name = str(manifest.get("artifact") or "")
if artifact_name != "pixverse-internal.zip":
raise InternalCliError(f"unexpected bundled internal CLI artifact name: {artifact_name!r}")
artifact_path = (manifest_path.parent / artifact_name).resolve()
if artifact_path.parent != manifest_path.parent.resolve():
raise InternalCliError("bundled internal CLI artifact path escapes its directory")
artifact = inspect_internal_cli_zip(
artifact_path,
minimum_version=minimum_version,
capabilities_schema=capabilities_schema,
expected_node_requirement=expected_node_requirement,
)
expected = {
"artifact_sha256": artifact.archive_sha256,
"package": artifact.package_name,
"version": artifact.version,
"core_version": artifact.core_version,
"node": artifact.node_requirement,
}
for key, value in expected.items():
if manifest.get(key) != value:
raise InternalCliError(
f"bundled internal CLI manifest {key} {manifest.get(key)!r} does not match {value!r}"
)
capabilities = manifest.get("capabilities")
if not isinstance(capabilities, dict):
raise InternalCliError("bundled internal CLI manifest lacks capabilities metadata")
expected_capabilities = {
"path": "dist/capabilities.json",
"schema_version": artifact.capabilities_schema,
"sha256": artifact.capabilities_sha256,
"command_count": artifact.command_count,
}
for key, value in expected_capabilities.items():
if capabilities.get(key) != value:
raise InternalCliError(
f"bundled internal CLI capabilities {key} {capabilities.get(key)!r} does not match {value!r}"
)
return artifact
source = discover_internal_cli_zip(root)
return inspect_internal_cli_zip(
source,
minimum_version=minimum_version,
capabilities_schema=capabilities_schema,
expected_node_requirement=expected_node_requirement,
)
def active_record(runtime_root: Path) -> dict[str, Any]:
path = runtime_root / ACTIVE_RECORD_FILENAME
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {}
return payload if isinstance(payload, dict) else {}
def active_install_root(runtime_root: Path) -> Path:
relative = str(active_record(runtime_root).get("root") or "")
if not relative:
return runtime_root / "versions" / "missing"
pure = PurePosixPath(relative)
if pure.is_absolute() or ".." in pure.parts:
return runtime_root / "versions" / "invalid"
candidate = runtime_root.joinpath(*pure.parts).resolve()
versions = (runtime_root / "versions").resolve()
if candidate == versions or versions not in candidate.parents:
return runtime_root / "versions" / "invalid"
return candidate
def active_matches_artifact(runtime_root: Path, artifact: InternalCliArtifact) -> bool:
record = active_record(runtime_root)
root = active_install_root(runtime_root)
return bool(
record.get("channel") == INTERNAL_CLI_CHANNEL
and record.get("artifact_sha256") == artifact.archive_sha256
and record.get("version") == artifact.version
and _installed_runtime_matches(root, artifact)
)
CommandRunner = Callable[[list[str], Path, int], subprocess.CompletedProcess[str]]
def _default_command_runner(argv: list[str], cwd: Path, timeout: int) -> subprocess.CompletedProcess[str]:
return subprocess.run(
argv,
cwd=cwd,
check=False,
text=True,
capture_output=True,
timeout=timeout,
)
def install_internal_cli(
artifact: InternalCliArtifact,
runtime_root: Path,
*,
node: str,
npm: str,
timeout: int = 600,
command_runner: CommandRunner = _default_command_runner,
) -> dict[str, Any]:
runtime_root.mkdir(parents=True, exist_ok=True)
lock_state_path = runtime_root / INSTALL_LOCK_STATE_FILENAME
try:
with canvas_sync_lock(lock_state_path, timeout_seconds=float(timeout)):
return _install_internal_cli_locked(
artifact,
runtime_root,
node=node,
npm=npm,
timeout=timeout,
command_runner=command_runner,
)
except CanvasSyncLockError as exc:
raise InternalCliInstallError(
f"timed out waiting for another internal CLI installation in {runtime_root}"
) from exc
def _install_internal_cli_locked(
artifact: InternalCliArtifact,
runtime_root: Path,
*,
node: str,
npm: str,
timeout: int,
command_runner: CommandRunner,
) -> dict[str, Any]:
versions = runtime_root / "versions"
versions.mkdir(parents=True, exist_ok=True)
version_slug = re.sub(r"[^A-Za-z0-9._-]+", "-", artifact.version).strip("-.") or "unknown"
destination = versions / f"{version_slug}-{artifact.archive_sha256[:12]}"
relative_root = destination.relative_to(runtime_root).as_posix()
active_root = active_install_root(runtime_root)
if active_matches_artifact(runtime_root, artifact) and _runtime_smoke_matches(
active_root,
artifact,
node=node,
command_runner=command_runner,
):
return _install_result(artifact, runtime_root, active_root, reused=True)
active_is_destination = active_root.resolve() == destination.resolve()
if (
not active_is_destination
and _installed_runtime_matches(destination, artifact)
and _runtime_smoke_matches(
destination,
artifact,
node=node,
command_runner=command_runner,
)
):
_write_active_record(runtime_root, artifact, relative_root)
return _install_result(artifact, runtime_root, destination, reused=True)
staging = Path(tempfile.mkdtemp(prefix=".internal-cli-staging-", dir=runtime_root))
try:
_extract_internal_cli(artifact, staging)
install_argv = [npm, "ci" if artifact.has_lockfile else "install", "--omit=dev", "--no-audit", "--no-fund"]
dependency_result = command_runner(install_argv, staging, timeout)
if dependency_result.returncode != 0:
detail = (dependency_result.stderr or dependency_result.stdout or "unknown npm error").strip()
raise InternalCliInstallError(f"internal CLI dependency installation failed: {detail}")
smoke_result = command_runner([str(_runtime_executable(staging)), "--version"], staging, 30)
reported_version = (smoke_result.stdout or smoke_result.stderr or "").strip()
if smoke_result.returncode != 0 or normalize_version(reported_version) != artifact.core_version:
raise InternalCliInstallError(
f"internal CLI smoke test reported {reported_version!r}, expected {artifact.core_version!r}"
)
install_record = artifact.bundle_manifest()
install_record.update({"installed_root": relative_root, "reported_version": reported_version})
(staging / INSTALL_RECORD_FILENAME).write_text(
json.dumps(install_record, indent=2, ensure_ascii=False, sort_keys=True) + "\n",
encoding="utf-8",
)
if destination.exists():
shutil.rmtree(destination)
os.replace(staging, destination)
_write_active_record(runtime_root, artifact, relative_root)
return _install_result(artifact, runtime_root, destination, reused=False)
finally:
if staging.exists():
shutil.rmtree(staging)
def _extract_internal_cli(artifact: InternalCliArtifact, destination: Path) -> None:
resolved_destination = destination.resolve()
with zipfile.ZipFile(artifact.path) as archive:
for item in archive.infolist():
pure = PurePosixPath(item.filename)
relative_parts = pure.parts[1:]
if not relative_parts:
continue
target = destination.joinpath(*relative_parts)
resolved_target = target.resolve()
if resolved_target == resolved_destination or resolved_destination not in resolved_target.parents:
raise InternalCliInstallError(
f"internal CLI extraction path escaped the staging directory: {item.filename!r}"
)
if item.is_dir():
target.mkdir(parents=True, exist_ok=True)
continue
target.parent.mkdir(parents=True, exist_ok=True)
with archive.open(item) as source, target.open("wb") as output:
shutil.copyfileobj(source, output)
mode = ((item.external_attr >> 16) & 0o777) or 0o644
target.chmod(mode)
def _installed_runtime_matches(destination: Path, artifact: InternalCliArtifact) -> bool:
try:
record = json.loads((destination / INSTALL_RECORD_FILENAME).read_text(encoding="utf-8"))
package = json.loads((destination / "package.json").read_text(encoding="utf-8"))
capabilities_path = destination / "dist" / "capabilities.json"
capabilities_sha256 = file_sha256(capabilities_path)
except (OSError, json.JSONDecodeError):
return False
dependencies = package.get("dependencies") if isinstance(package, dict) else None
dependencies_ready = not isinstance(dependencies, dict) or not dependencies or (destination / "node_modules").is_dir()
record_capabilities = record.get("capabilities") if isinstance(record, dict) else None
return bool(
isinstance(record, dict)
and record.get("artifact_sha256") == artifact.archive_sha256
and record.get("version") == artifact.version
and isinstance(record_capabilities, dict)
and record_capabilities.get("sha256") == artifact.capabilities_sha256
and isinstance(package, dict)
and package.get("name") == artifact.package_name
and package.get("version") == artifact.version
and (package.get("bin") or {}).get("pixverse") == "./dist/index.js"
and capabilities_sha256 == artifact.capabilities_sha256
and dependencies_ready
and (destination / "package.json").is_file()
and (destination / "dist" / "index.js").is_file()
and _runtime_executable_is_usable(destination)
)
def _runtime_smoke_matches(
destination: Path,
artifact: InternalCliArtifact,
*,
node: str,
command_runner: CommandRunner,
) -> bool:
try:
if not _runtime_executable_is_usable(destination):
return False
result = command_runner([str(_runtime_executable(destination)), "--version"], destination, 30)
except (OSError, subprocess.SubprocessError):
return False
reported_version = (result.stdout or result.stderr or "").strip()
return result.returncode == 0 and normalize_version(reported_version) == artifact.core_version
def _write_active_record(
runtime_root: Path,
artifact: InternalCliArtifact,
relative_root: str,
) -> None:
record = {
"schema_version": 1,
"channel": INTERNAL_CLI_CHANNEL,
"distribution": "standalone-zip",
"version": artifact.version,
"core_version": artifact.core_version,
"artifact_sha256": artifact.archive_sha256,
"capabilities_sha256": artifact.capabilities_sha256,
"root": relative_root,
}
handle = tempfile.NamedTemporaryFile(
mode="w",
encoding="utf-8",
prefix=".active-",
suffix=".json",
dir=runtime_root,
delete=False,
)
temporary = Path(handle.name)
try:
json.dump(record, handle, indent=2, ensure_ascii=False, sort_keys=True)
handle.write("\n")
handle.close()
os.replace(temporary, runtime_root / ACTIVE_RECORD_FILENAME)
finally:
if not handle.closed:
handle.close()
temporary.unlink(missing_ok=True)
def _install_result(
artifact: InternalCliArtifact,
runtime_root: Path,
install_root: Path,
*,
reused: bool,
) -> dict[str, Any]:
return {
"channel": INTERNAL_CLI_CHANNEL,
"source": "bundled-zip",
"version": artifact.version,
"core_version": artifact.core_version,
"artifact_sha256": artifact.archive_sha256,
"runtime": str(runtime_root),
"install_root": str(install_root),
"executable": str(install_root / ("pixverse.cmd" if os.name == "nt" else "pixverse")),
"reused": reused,
}
SHA-256: fd62882f2aaea8d525f22205757fe509aee3ea42452c28d569b734eab0cb8bf2