motionloom 2.0.0 → 2.1.0

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/CODE_OF_CONDUCT.md +19 -0
  3. package/CONTRIBUTING.md +65 -0
  4. package/README.md +187 -134
  5. package/ROADMAP.md +32 -0
  6. package/SECURITY.md +27 -0
  7. package/SKILL.md +33 -8
  8. package/SUPPORT.md +23 -0
  9. package/agent-card.json +21 -6
  10. package/bin/motionloom.mjs +23 -5
  11. package/docs/STATUS.md +33 -0
  12. package/docs/audits/2.1.0-deep-stress-evaluation.md +97 -0
  13. package/docs/audits/data/2.1.0-deep-stress-6900.json +329 -0
  14. package/docs/audits/data/deep-stress-latest.json +329 -0
  15. package/docs/audits/external-project-corpus-2026-08-13.md +26 -0
  16. package/docs/releases/2.1.0.md +23 -0
  17. package/docs/releases/npm-publish-from-workstation.md +6 -6
  18. package/package.json +52 -26
  19. package/references/intelligence-core.md +1 -1
  20. package/schemas/project-memory.schema.json +180 -0
  21. package/scripts/analyze.py +56 -0
  22. package/scripts/capture-runtime-telemetry.py +119 -0
  23. package/scripts/devlab.py +126 -0
  24. package/scripts/docs-audit.py +96 -0
  25. package/scripts/eval-intelligence.py +23 -0
  26. package/scripts/eval-projects.py +156 -0
  27. package/scripts/intelligence.py +106 -6
  28. package/scripts/pr.py +150 -0
  29. package/scripts/prepack-clean.mjs +37 -0
  30. package/scripts/project-memory.py +483 -0
  31. package/scripts/project_memory_loader.py +31 -0
  32. package/scripts/release-verify.py +52 -0
  33. package/scripts/render.py +65 -0
  34. package/scripts/report.py +44 -2
  35. package/scripts/review-hook.py +13 -2
  36. package/scripts/skill-doctor.py +12 -2
  37. package/scripts/to-dotlottie.mjs +26 -20
  38. package/src/core/analyzer.py +174 -25
  39. package/tests/evals/intelligence-cases.json +10 -0
  40. package/tests/evals/project-corpus.json +51 -0
  41. package/tests/scripts/run_tests.py +52 -1
  42. package/tests/scripts/test_project_memory.py +129 -0
@@ -0,0 +1,31 @@
1
+ """Small import-safe bridge for the cross-platform analyzer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ from pathlib import Path
7
+
8
+
9
+ def _module():
10
+ path = Path(__file__).with_name("project-memory.py")
11
+ spec = importlib.util.spec_from_file_location("motionloom_project_memory", path)
12
+ if spec is None or spec.loader is None:
13
+ raise RuntimeError(f"Cannot load Project Memory module: {path}")
14
+ module = importlib.util.module_from_spec(spec)
15
+ spec.loader.exec_module(module)
16
+ return module
17
+
18
+
19
+ def refresh_if_present(root: Path, context_path: Path, initialize: bool = False):
20
+ module = _module()
21
+ memory_path = (root / ".motionloom" / "project-memory.json").resolve()
22
+ if not memory_path.exists():
23
+ if not initialize:
24
+ return None
25
+ memory = module.base_memory(root, str(context_path))
26
+ module.write_atomic(memory_path, memory)
27
+ return {"status": "initialized", "memory_path": str(memory_path), "freshness": memory["freshness"]}
28
+ memory = module.load_or_fail(memory_path)
29
+ refreshed = module.refresh_memory(memory, root, str(context_path))
30
+ module.write_atomic(memory_path, refreshed)
31
+ return {"status": refreshed["freshness"]["status"], "memory_path": str(memory_path), "freshness": refreshed["freshness"]}
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env python3
2
+ """Verify the immutable metadata chain before an npm/GitHub release."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import re
9
+ import sys
10
+ from pathlib import Path
11
+
12
+
13
+ ROOT = Path(__file__).resolve().parents[1]
14
+
15
+
16
+ def main() -> int:
17
+ parser = argparse.ArgumentParser(description="Verify package version, changelog and release note alignment.")
18
+ parser.add_argument("--expected-version", default="")
19
+ parser.add_argument("--package", default=str(ROOT / "package.json"))
20
+ parser.add_argument("--changelog", default=str(ROOT / "CHANGELOG.md"))
21
+ parser.add_argument("--release-note", default="")
22
+ parser.add_argument("--tag", default="", help="Optional Git tag; accepts v<version> or <version>")
23
+ args = parser.parse_args()
24
+
25
+ errors: list[str] = []
26
+ package_path = Path(args.package).resolve()
27
+ package = json.loads(package_path.read_text(encoding="utf-8"))
28
+ actual = str(package.get("version", ""))
29
+ expected = str(args.expected_version or actual).removeprefix("v")
30
+ if actual != expected:
31
+ errors.append(f"package version {actual!r} does not match expected {expected!r}")
32
+
33
+ changelog = Path(args.changelog).resolve()
34
+ if not re.search(rf"^## \[{re.escape(expected)}\](?:\s|$)", changelog.read_text(encoding="utf-8"), re.MULTILINE):
35
+ errors.append(f"CHANGELOG.md has no heading for [{expected}]")
36
+
37
+ note = Path(args.release_note) if args.release_note else ROOT / "docs/releases" / f"{expected}.md"
38
+ if not note.is_absolute():
39
+ note = ROOT / note
40
+ if not note.is_file():
41
+ errors.append(f"release note is missing: {note.relative_to(ROOT) if note.is_relative_to(ROOT) else note}")
42
+
43
+ if args.tag and args.tag.removeprefix("v") != expected:
44
+ errors.append(f"tag {args.tag!r} does not match version {expected!r}")
45
+
46
+ report = {"status": "fail" if errors else "pass", "version": actual, "expected_version": expected, "errors": errors}
47
+ print(json.dumps(report, indent=2))
48
+ return 1 if errors else 0
49
+
50
+
51
+ if __name__ == "__main__":
52
+ raise SystemExit(main())
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ MotionLoom render entrypoint.
4
+
5
+ Style contract: evidence-first, explicit runtime mode and no hidden approval
6
+ side effects. This is the cross-platform equivalent of render.sh and delegates
7
+ to the canonical Python snapshot renderer without requiring Bash.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import os
14
+ import re
15
+ import sys
16
+ from pathlib import Path
17
+
18
+
19
+ SCENE_RE = re.compile(r"^[A-Za-z0-9._-]+$")
20
+
21
+
22
+ def main() -> int:
23
+ parser = argparse.ArgumentParser(description="Render MotionLoom runtime snapshots")
24
+ parser.add_argument("scene")
25
+ parser.add_argument("--repo", default=None, help="Repository root; defaults to the script parent")
26
+ parser.add_argument("--progress", default="0,50,100")
27
+ parser.add_argument("--allow-placeholder", action="store_true")
28
+ args = parser.parse_args()
29
+
30
+ if not SCENE_RE.fullmatch(args.scene):
31
+ parser.error("scene id contains unsafe path characters")
32
+
33
+ repo = Path(args.repo).expanduser().resolve() if args.repo else Path(__file__).resolve().parents[1]
34
+ scene_dir = repo / "src" / "output" / args.scene
35
+ if not scene_dir.is_dir():
36
+ print(f"error: scene directory not found: {scene_dir}", file=sys.stderr)
37
+ return 1
38
+
39
+ try:
40
+ progress = [int(value) for value in args.progress.split(",") if value.strip()]
41
+ except ValueError:
42
+ print("error: progress must be a comma-separated list of integers", file=sys.stderr)
43
+ return 1
44
+
45
+ if not progress or any(value < 0 or value > 100 for value in progress):
46
+ print("error: progress values must be between 0 and 100", file=sys.stderr)
47
+ return 1
48
+
49
+ # Import by path-independent package layout. The repository root is added
50
+ # only for this process; no cwd or shell-specific import assumptions.
51
+ sys.path.insert(0, str(repo))
52
+ from src.core.snapshot import render_snapshots # pylint: disable=import-outside-toplevel
53
+
54
+ result = render_snapshots(
55
+ args.scene,
56
+ scene_dir,
57
+ progress,
58
+ allow_placeholder=args.allow_placeholder or os.environ.get("ALLOW_PLACEHOLDER") == "1",
59
+ )
60
+ print(__import__("json").dumps(result, indent=2))
61
+ return 0
62
+
63
+
64
+ if __name__ == "__main__":
65
+ raise SystemExit(main())
package/scripts/report.py CHANGED
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
  import argparse
7
7
  import hashlib
8
8
  import json
9
+ import shutil
9
10
  import sys
10
11
  from datetime import datetime, timezone
11
12
  from pathlib import Path
@@ -49,6 +50,39 @@ def read_json(path: Path, default: dict | list | None = None):
49
50
  return json.loads(path.read_text(encoding="utf-8"))
50
51
 
51
52
 
53
+ def project_memory_path() -> Path:
54
+ return ROOT / ".motionloom" / "project-memory.json"
55
+
56
+
57
+ def memory_summary() -> dict | None:
58
+ path = project_memory_path()
59
+ if not path.is_file():
60
+ return None
61
+ try:
62
+ memory = read_json(path)
63
+ digest = hashlib.sha256(path.read_bytes()).hexdigest()
64
+ except (OSError, json.JSONDecodeError):
65
+ return {"path": ".motionloom/project-memory.json", "status": "invalid"}
66
+ freshness = memory.get("freshness") if isinstance(memory, dict) else {}
67
+ return {
68
+ "path": ".motionloom/project-memory.json",
69
+ "snapshot_path": "project-memory.json",
70
+ "memory_id": memory.get("memory_id"),
71
+ "status": freshness.get("status", "invalid"),
72
+ "sha256": digest,
73
+ "updated_at": memory.get("updated_at"),
74
+ }
75
+
76
+
77
+ def sync_memory_snapshot(task_dir: Path) -> dict | None:
78
+ source = project_memory_path()
79
+ if not source.is_file():
80
+ return None
81
+ destination = task_dir / "project-memory.json"
82
+ shutil.copy2(source, destination)
83
+ return memory_summary()
84
+
85
+
52
86
  def has_symlink_component(path: Path) -> bool:
53
87
  current = path
54
88
  while True:
@@ -78,6 +112,7 @@ def candidate_is_current(candidate: dict) -> bool:
78
112
  def init_task(args: argparse.Namespace) -> int:
79
113
  task_dir = Path(args.output or ROOT / "artifacts" / args.task_id).resolve()
80
114
  timestamp = now()
115
+ memory = memory_summary()
81
116
  task = {
82
117
  "schema_version": "1.0",
83
118
  "task_id": args.task_id,
@@ -92,6 +127,8 @@ def init_task(args: argparse.Namespace) -> int:
92
127
  "created_at": timestamp,
93
128
  "updated_at": timestamp,
94
129
  }
130
+ if memory:
131
+ task["project_memory"] = memory
95
132
  report = {
96
133
  "report_version": "1.0",
97
134
  "task_id": args.task_id,
@@ -103,7 +140,7 @@ def init_task(args: argparse.Namespace) -> int:
103
140
  "problems": [],
104
141
  "structure_review": {"missing_files": [], "broken_references": [], "untracked_artifacts": []},
105
142
  "browser_review": [],
106
- "next_agent": [{"agent": args.agent, "action": "Run project analysis and populate context before generation."}],
143
+ "next_agent": [{"agent": args.agent, "action": "Recover Project Memory and run project analysis before generation.", "evidence_needed": ["project-memory.json", "project-context.json"]}],
107
144
  "generated_at": timestamp,
108
145
  }
109
146
  handoff = {
@@ -114,7 +151,7 @@ def init_task(args: argparse.Namespace) -> int:
114
151
  "state": "created",
115
152
  "summary": "New animation task initialized.",
116
153
  "next_actions": [{"action": "Analyze host project context", "skill": "motionloom", "evidence_needed": ["project-context.json"]}],
117
- "required_artifacts": ["task.json", "execution-report.json", *EVIDENCE_ARTIFACTS],
154
+ "required_artifacts": ["task.json", "execution-report.json", *EVIDENCE_ARTIFACTS, *( ["project-memory.json"] if memory else [] )],
118
155
  "blockers": [],
119
156
  }
120
157
  write_json(task_dir / "task.json", task)
@@ -122,6 +159,8 @@ def init_task(args: argparse.Namespace) -> int:
122
159
  write_json(task_dir / "issue-register.json", {"version": "1.0", "task_id": args.task_id, "issues": []})
123
160
  write_json(task_dir / "handoff.json", handoff)
124
161
  write_json(task_dir / "artifact-manifest.json", {"manifest_version": "1.0", "task_id": args.task_id, "generated_at": timestamp, "artifacts": []})
162
+ if memory:
163
+ sync_memory_snapshot(task_dir)
125
164
  (task_dir / "decision-log.jsonl").write_text("", encoding="utf-8")
126
165
  print(json.dumps({"status": "created", "task_id": args.task_id, "task_dir": str(task_dir)}, ensure_ascii=False))
127
166
  return 0
@@ -130,6 +169,7 @@ def init_task(args: argparse.Namespace) -> int:
130
169
  def collect(args: argparse.Namespace) -> int:
131
170
  task_dir = Path(args.task_dir).resolve()
132
171
  task = read_json(task_dir / "task.json")
172
+ sync_memory_snapshot(task_dir)
133
173
  excluded = {"artifact-manifest.json", "execution-report.json", "decision-log.jsonl"}
134
174
  artifacts = []
135
175
  for path in sorted(task_dir.rglob("*")):
@@ -146,6 +186,8 @@ def collect(args: argparse.Namespace) -> int:
146
186
  handoff = read_json(handoff_path)
147
187
  required = set(handoff.get("required_artifacts", []))
148
188
  required.update(name for name in EVIDENCE_ARTIFACTS if (task_dir / name).is_file())
189
+ if (task_dir / "project-memory.json").is_file():
190
+ required.add("project-memory.json")
149
191
  handoff["required_artifacts"] = sorted(required)
150
192
  write_json(handoff_path, handoff)
151
193
  print(json.dumps({"status": "collected", "task_id": manifest["task_id"], "artifact_count": len(artifacts)}, ensure_ascii=False))
@@ -7,6 +7,7 @@ import argparse
7
7
  import hashlib
8
8
  import json
9
9
  import re
10
+ import shutil
10
11
  import subprocess
11
12
  import sys
12
13
  from datetime import datetime, timedelta, timezone
@@ -68,6 +69,11 @@ def prepare(args: argparse.Namespace) -> int:
68
69
  if not scene:
69
70
  raise ValueError("task.json requires scene")
70
71
  scene_dir, source_path, render_meta, context_path = paths(task_dir, task)
72
+ memory_path = ROOT / ".motionloom" / "project-memory.json"
73
+ memory = read_json(memory_path, {}) if memory_path.is_file() else {}
74
+ memory_status = (memory.get("freshness") or {}).get("status")
75
+ if memory_path.is_file() and memory_status not in {"fresh", None}:
76
+ raise ValueError(f"project memory is {memory_status}; run motionloom memory refresh/analyze before browser review")
71
77
  spec = read_json(scene_dir / "motion-spec.json")
72
78
  context_hash = (spec.get("context_binding") or {}).get("context_sha256") or sha256(context_path)
73
79
  source_hash = sha256(source_path)
@@ -92,6 +98,9 @@ def prepare(args: argparse.Namespace) -> int:
92
98
  "prepared_at": now(),
93
99
  "expires_at": (datetime.now(timezone.utc) + CANDIDATE_TTL).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
94
100
  }
101
+ if memory_path.is_file():
102
+ candidate["project_memory_sha256"] = sha256(memory_path)
103
+ candidate["project_memory_id"] = memory.get("memory_id")
95
104
  (scene_dir / "browser-review.json").write_text(json.dumps(candidate, indent=2) + "\n", encoding="utf-8")
96
105
  task["state"] = "review_required"
97
106
  task["updated_at"] = now()
@@ -109,9 +118,11 @@ def prepare(args: argparse.Namespace) -> int:
109
118
  report_path.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
110
119
  handoff_path = task_dir / "handoff.json"
111
120
  handoff = read_json(handoff_path)
112
- handoff.update({"state": "review_required", "to_agent": "browser-review-agent", "summary": "Open the exact rendered candidate in the internal Dev Lab and obtain user approval before PR.", "next_actions": [{"action": "Open internal Dev Lab candidate", "kind": "browser_review", "agent": "browser-review-agent", "skill": "browser-review", "url": url, "candidate_id": cid, "requires_user_approval": True, "evidence_needed": ["review.json"], "output_artifacts": ["review.json"]}], "required_artifacts": sorted(set(handoff.get("required_artifacts", []) + ["browser-review.json", "review.json", "semantic-lint-benchmark.json", "evidence-verifier-report.json", "runtime-adapters/runtime-evidence.json"]))})
121
+ if memory_path.is_file():
122
+ shutil.copy2(memory_path, task_dir / "project-memory.json")
123
+ handoff.update({"state": "review_required", "to_agent": "browser-review-agent", "summary": "Open the exact rendered candidate in the internal Dev Lab and obtain user approval before PR.", "next_actions": [{"action": "Open internal Dev Lab candidate", "kind": "browser_review", "agent": "browser-review-agent", "skill": "browser-review", "url": url, "candidate_id": cid, "requires_user_approval": True, "evidence_needed": ["review.json"], "output_artifacts": ["review.json"]}], "required_artifacts": sorted(set(handoff.get("required_artifacts", []) + ["browser-review.json", "review.json", "semantic-lint-benchmark.json", "evidence-verifier-report.json", "runtime-adapters/runtime-evidence.json"] + (["project-memory.json"] if memory_path.is_file() else [])))})
113
124
  handoff_path.write_text(json.dumps(handoff, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
114
- subprocess.run(["bash", str(ROOT / "scripts/devlab.sh"), scene, "--prepare-only", str(task_dir)], check=True, capture_output=True, text=True)
125
+ subprocess.run([sys.executable, str(ROOT / "scripts/devlab.py"), scene, "--prepare-only", "--task-dir", str(task_dir)], check=True, capture_output=True, text=True)
115
126
  subprocess.run([sys.executable, str(ROOT / "scripts/report.py"), "collect", "--task-dir", str(task_dir)], check=True, capture_output=True, text=True)
116
127
  print(json.dumps({"status": "review_required", "task_id": task["task_id"], "candidate_id": cid, "url": url, "agent": "browser-review-agent", "action": "Open this exact URL in the internal browser; ask the user to inspect frames 0/50/100 and approve or request changes.", "requires_user_approval": True, "output_artifacts": ["review.json"]}, ensure_ascii=False))
117
128
  return 0
@@ -13,7 +13,16 @@ from pathlib import Path
13
13
  ROOT = Path(__file__).resolve().parents[1]
14
14
  REQUIRED_FILES = ["SKILL.md", "agent-card.json", "package.json"]
15
15
  REQUIRED_DIRS = ["scripts", "templates", "references", "schemas"]
16
- REQUIRED_SCRIPT_FILES = ["scripts/report-contract.py", "scripts/review-hook.py", "scripts/quality-gate.py", "scripts/runtime-adapters.mjs"]
16
+ REQUIRED_SCRIPT_FILES = [
17
+ "scripts/report-contract.py",
18
+ "scripts/review-hook.py",
19
+ "scripts/quality-gate.py",
20
+ "scripts/runtime-adapters.mjs",
21
+ "scripts/project-memory.py",
22
+ "scripts/project_memory_loader.py",
23
+ "scripts/analyze.py",
24
+ "scripts/devlab.py",
25
+ ]
17
26
  REQUIRED_SCHEMAS = [
18
27
  "task.schema.json",
19
28
  "execution-report.schema.json",
@@ -26,6 +35,7 @@ REQUIRED_SCHEMAS = [
26
35
  "provenance.schema.json",
27
36
  "capability-registry.schema.json",
28
37
  "motion-ir.schema.json",
38
+ "project-memory.schema.json",
29
39
  ]
30
40
 
31
41
 
@@ -121,7 +131,7 @@ def run() -> int:
121
131
  package_path = ROOT / "package.json"
122
132
  try:
123
133
  package = json.loads(package_path.read_text(encoding="utf-8"))
124
- for script in ("test", "validate", "doctor", "report", "report:check", "review"):
134
+ for script in ("test", "validate", "doctor", "report", "report:check", "review", "memory:bootstrap", "memory:recover", "memory:validate", "devlab", "pack:dotlottie"):
125
135
  if script not in package.get("scripts", {}):
126
136
  warnings.append({"code": "missing_package_script", "message": f"package.json has no {script} script."})
127
137
  except (FileNotFoundError, json.JSONDecodeError) as exc:
@@ -6,9 +6,8 @@
6
6
  */
7
7
  import crypto from "node:crypto";
8
8
  import fs from "node:fs";
9
- import os from "node:os";
10
9
  import path from "node:path";
11
- import { execFileSync } from "node:child_process";
10
+ import { strFromU8, zipSync, unzipSync } from "fflate";
12
11
 
13
12
  function arg(name, fallback = undefined) {
14
13
  const index = process.argv.indexOf(name);
@@ -43,17 +42,14 @@ if (!/^[a-zA-Z0-9._ -]+$/.test(animationId)) {
43
42
  }
44
43
  readJson(sourcePath);
45
44
 
46
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "animation-skill-dotlottie-"));
47
- const archiveRoot = path.join(tmp, "archive");
48
- fs.mkdirSync(path.join(archiveRoot, "a"), { recursive: true });
49
- fs.copyFileSync(sourcePath, path.join(archiveRoot, "a", `${animationId}.json`));
45
+ const archive = {
46
+ [`a/${animationId}.json`]: fs.readFileSync(sourcePath),
47
+ };
50
48
 
51
49
  const optionalDirectories = ["i", "t", "s", "f"];
52
50
  for (const directory of optionalDirectories) {
53
51
  const candidate = path.join(sceneDir, "dotlottie", directory);
54
- if (fs.existsSync(candidate)) {
55
- fs.cpSync(candidate, path.join(archiveRoot, directory), { recursive: true });
56
- }
52
+ if (fs.existsSync(candidate)) addTree(candidate, directory);
57
53
  }
58
54
 
59
55
  const dotManifest = {
@@ -62,20 +58,13 @@ const dotManifest = {
62
58
  initial: { animation: animationId },
63
59
  animations: [{ id: animationId }],
64
60
  };
65
- fs.writeFileSync(
66
- path.join(archiveRoot, "manifest.json"),
67
- `${JSON.stringify(dotManifest, null, 2)}\n`,
68
- "utf8",
69
- );
61
+ archive["manifest.json"] = Buffer.from(`${JSON.stringify(dotManifest, null, 2)}\n`, "utf8");
70
62
 
71
63
  fs.mkdirSync(path.dirname(output), { recursive: true });
72
64
  if (fs.existsSync(output)) fs.rmSync(output, { force: true });
73
- execFileSync("zip", ["-X", "-q", "-r", output, "."], { cwd: archiveRoot });
65
+ fs.writeFileSync(output, zipSync(archive, { level: 6 }));
74
66
 
75
- const entries = execFileSync("unzip", ["-Z1", output], { encoding: "utf8" })
76
- .trim()
77
- .split(/\r?\n/)
78
- .filter(Boolean);
67
+ const entries = Object.keys(unzipSync(fs.readFileSync(output))).sort();
79
68
  if (!entries.includes("manifest.json")) throw new Error("archive missing manifest.json");
80
69
  if (!entries.includes(`a/${animationId}.json`)) {
81
70
  throw new Error("archive missing initial animation payload");
@@ -95,5 +84,22 @@ console.log(JSON.stringify({
95
84
  }, null, 2));
96
85
 
97
86
  function readJsonFromZip(file, entry) {
98
- return JSON.parse(execFileSync("unzip", ["-p", file, entry], { encoding: "utf8" }));
87
+ const payload = unzipSync(fs.readFileSync(file))[entry];
88
+ if (!payload) throw new Error(`archive entry is missing: ${entry}`);
89
+ return JSON.parse(strFromU8(payload));
90
+ }
91
+
92
+ function addTree(directory, archivePrefix) {
93
+ const entries = fs.readdirSync(directory, { withFileTypes: true });
94
+ for (const entry of entries) {
95
+ const sourceEntry = path.join(directory, entry.name);
96
+ const archiveEntry = `${archivePrefix}/${entry.name.replaceAll("\\", "/")}`;
97
+ const stat = fs.lstatSync(sourceEntry);
98
+ if (stat.isSymbolicLink()) continue;
99
+ if (stat.isDirectory()) {
100
+ addTree(sourceEntry, archiveEntry);
101
+ } else if (stat.isFile()) {
102
+ archive[archiveEntry] = fs.readFileSync(sourceEntry);
103
+ }
104
+ }
99
105
  }