Files
Agent-System-Dev/scripts/workspace/npm.py
T
weichen b450e51c40 Add safe component editing and explicit legacy workspace transition
Why: Preserve a single Dev entry and independent repository ownership while allowing active feature branches to migrate without losing source or Git state.

Evidence: SELF 29 real Git/Go workspace tests and ordinary component evidence/lock tests passed; detailed RED/GREEN runs are recorded by make evidence in the task card.

Impact: Add fixed cloning, selected-version baselines, development overlays, npm source resolution, and independently preserved legacy snapshots. No other active worktree is modified.

Refs: Versioned dependencies U2/U5; R15; feature/dependency-workspace
2026-10-13 15:58:23 +00:00

63 lines
3.2 KiB
Python

"""Resolve editable npm source from a consumer's Git package lock, without npm hooks."""
from pathlib import Path
from urllib.parse import unquote, urlsplit, urlunsplit
from .common import WorkspaceError, read_json, safe_path
from .manifest import SHA, validate_repo
def repository_identity(value):
value = value.removeprefix("git+")
parsed = urlsplit(value)
if parsed.scheme == "file":
if parsed.netloc not in ("", "localhost"):
raise WorkspaceError("npm Git origin must be local or approved")
return str(Path(unquote(parsed.path)).resolve())
validate_repo(value)
return value
class Npm:
def __init__(self, workspace):
self.workspace = workspace
def resolve(self, component):
package = component.get("package_name")
if not package:
return None
selected = []
consumers = sorted(self.workspace.manifest.select(), key=lambda entry: ("frontend" not in entry["id"].lower(), entry["id"]))
for consumer in consumers:
if consumer["role"] != "application":
continue
root = safe_path(self.workspace.root, consumer["path"])
path = safe_path(root, "package-lock.json")
if not path.exists():
continue
lock = read_json(path)
if not isinstance(lock, dict) or lock.get("lockfileVersion") not in (2, 3) or not isinstance(lock.get("packages"), dict):
raise WorkspaceError(f"unsupported npm lock for {consumer['id']}")
for location, entry in lock["packages"].items():
if location != "node_modules/" + package and not location.endswith("/node_modules/" + package):
continue
if not isinstance(entry, dict) or entry.get("link"):
raise WorkspaceError(f"local npm link cannot establish version baseline: {package}")
resolved = entry.get("resolved", "")
if not isinstance(resolved, str) or not resolved.startswith("git+"):
raise WorkspaceError(f"npm source needs a fixed approved Git origin: {package}")
parsed = urlsplit(resolved)
if not SHA.fullmatch(parsed.fragment):
raise WorkspaceError(f"npm Git source must pin a full commit: {package}")
origin = urlunsplit((parsed.scheme, parsed.netloc, parsed.path, parsed.query, ""))
if repository_identity(origin) != repository_identity(component["repo"]):
raise WorkspaceError(f"npm Git origin mismatch: {package}")
selected.append((parsed.fragment, {"kind": "npm-lock", "package": package,
"version": entry.get("version"), "resolved": resolved,
"consumer": consumer["id"], "lock_path": str(path.relative_to(self.workspace.root))}))
if not selected:
return None
if len({entry[0] for entry in selected}) != 1:
raise WorkspaceError(f"consumers select conflicting npm Git commits for {package}; owner must choose compatible versions")
commit, source = selected[0]
return commit, dict(source, selections=[entry[1] for entry in selected])