From e04e0b7f144188323ca863284b8a1ec194920505 Mon Sep 17 00:00:00 2001 From: minhn4 Date: Sat, 15 Aug 2026 12:12:45 +0700 Subject: [PATCH] fix(sandbox): build the guest image from the office bundle's proven versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit install-office.sh was a loose re-interpretation of codev-office-ubuntu-setup.sh rather than a translation of it. It apt-installed pandoc and LibreOffice, took whatever the dotnet 8.0 channel currently served, and pip-installed the Python set from PyPI — so the guest ran pandoc 2.17.1.1 against the bundle's 3.10.1, LibreOffice 7.4.7.2 against 26.2.5, SDK 8.0.424 against 8.0.423, and unpinned Python packages. It also ignored two bundle components outright: the offline NuGet feed, which the docx skill's CLI restores DocumentFormat.OpenXml from and which a sealed guest cannot reach nuget.org to replace, and Playwright's Chromium, without which the pdf skill has no browser to render covers with. Versions now come off the bundle's own filenames, so refreshing the bundle moves the image with it. The bundle's x86_64 binaries are used verbatim on amd64; arm64 fetches the same version arch-correct (LibreOffice's aarch64 deb set, pandoc's arm64 deb, dotnet-install --version, and PyPI under a constraints file generated from the bundle's own cp311 wheels). A version the bundle pins that cannot be matched on an arch is a hard failure, never a substitution. Two defects found by verifying the built image rather than the script: LibreOffice was installed but could not execute. The TDF debs declare almost no dependencies, so `dpkg -i` succeeds and the `-f` fallback never fires, leaving system libraries absent — and LibreOffice dlopens several of them (libnss3's libssl3.so, libcairo.so.2), so they are invisible to ldd and surface only as "error while loading shared libraries" at first use. It appeared to work only because Playwright's install-deps happened to pull the same libraries in later. Debian packages the same software, so its libreoffice-core dependencies are now taken as the authoritative list. The build additionally converts a document and fails if no PDF comes out: the previous gate was `command -v soffice`, which passes happily on a binary that cannot load. Skills unzipped a directory too deep, because the zips carry their own top-level / dir. Discovery survived it (its glob is recursive and these skills name themselves in frontmatter), but a skill without a frontmatter name would silently fail to load. Staged and moved as the setup script does, and the check is now for SKILL.md at the skill root rather than the directory existing. The bundle is bind-mounted rather than COPYed, since committing 1.2 GB to a layer that the install step then cannot delete is pure waste. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 15 + packages/codev-sandbox/image/Dockerfile | 8 +- .../image/office/install-office.sh | 318 +++++++++++++++--- packages/codev-sandbox/script/build-image.ts | 23 +- 4 files changed, 301 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac6fd259a3..0300f29034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,21 @@ file is the human-readable summary of what changed and why. ### Desktop / Web UI +- **The office skills now work inside a sandbox** — the document skills + (Word, Excel, PowerPoint, PDF) were installed into the sandbox image, but + several pieces they depend on were not, so they failed once you actually + used them. LibreOffice was installed in a state where it could not start at + all, which broke every document conversion. The .NET packages the Word skill + compiles against were absent, and a sandbox has no route to the internet to + fetch them, so that skill could not run. The browser the PDF skill uses to + render covers was missing entirely. All three now ship in the image, and the + image build refuses to finish if LibreOffice cannot convert a document, so + this class of breakage cannot reach you again. The skills also now match the + versions they were tested against — LibreOffice 26.2.5, pandoc 3.10.1, .NET + 8.0.423 and a fixed set of Python packages — instead of whichever versions + the build happened to pick up. **Requires the updated sandbox image**: use a + sandbox's **Reset** button to pick it up. Sandboxes are noticeably larger to + download as a result. - **Sandboxes can be stopped** — a sandbox ran from the moment the app started until it was deleted, and every sandbox on the machine booted at launch, so three sandboxes meant three virtual machines holding memory and CPU whether diff --git a/packages/codev-sandbox/image/Dockerfile b/packages/codev-sandbox/image/Dockerfile index 34dd9af7c2..d004cca351 100644 --- a/packages/codev-sandbox/image/Dockerfile +++ b/packages/codev-sandbox/image/Dockerfile @@ -36,10 +36,10 @@ RUN cd /opt && npm install --no-save --no-audit --no-fund jsonc-parser @lydell/n # because sandbox homes are fresh. Skipped when OFFICE=0. FROM base AS office ARG OFFICE=1 -COPY office/install-office.sh /tmp/install-office.sh -COPY office/bundle/ /tmp/office-bundle/ -RUN if [ "$OFFICE" = "1" ]; then bash /tmp/install-office.sh /tmp/office-bundle; fi \ - && rm -rf /tmp/office-bundle /tmp/install-office.sh +# Bind-mounted, not COPYed: the bundle is well over a gigabyte, and a COPY +# would commit all of it to a layer that the install step cannot delete again. +RUN --mount=type=bind,source=office,target=/office \ + if [ "$OFFICE" = "1" ]; then bash /office/install-office.sh /office/bundle; fi # Guest layer: the vz init (PID 1) and the mount points every backend expects. # Harmless in the bwrap/wsl tars. diff --git a/packages/codev-sandbox/image/office/install-office.sh b/packages/codev-sandbox/image/office/install-office.sh index f071fafb12..d792b57608 100644 --- a/packages/codev-sandbox/image/office/install-office.sh +++ b/packages/codev-sandbox/image/office/install-office.sh @@ -1,77 +1,295 @@ #!/usr/bin/env bash # System-wide translation of codev-office-ubuntu-setup.sh for the sandbox # guest image. Runs inside the Docker build as root; $1 is the staged bundle -# dir (skill zips + pure-JS node-module zips — arch-independent parts only; -# dotnet/pandoc/libreoffice/wheels are fetched arch-correct right here, since -# the BUILD machine has internet even though the sealed guest will not). +# dir (the whole codev-office-ubuntu bundle, minus its macOS-only artifacts). +# +# The bundle is the proven set: the office skills are tested against exactly +# these component versions, so these are the versions installed here. Every +# version is read off the bundle's own filenames rather than pinned again in +# this file, so refreshing the bundle moves the image with it. +# +# The bundle's binaries are x86_64-only. On arm64 the same version is fetched +# arch-correct instead (the build machine has internet; the sealed guest will +# not) — the arch changes, the version never does. Anything the bundle pins +# that cannot be matched on this arch is a hard failure, not a substitution. +# +# The per-user layout of the setup script becomes system-wide here, because +# sandbox homes are fresh: /etc/skel seeds each one (vz at image build, other +# backends from their boot scripts). set -euxo pipefail BUNDLE="${1:-/tmp/office-bundle}" ARCH="$(dpkg --print-architecture)" +OFFICE_HOME=/opt/codev-office -# pandoc + LibreOffice + fonts from Debian (available on amd64 and arm64). +bundled() { ls "$BUNDLE"/$1 2>/dev/null | head -1 || true; } +require() { [ -n "$2" ] || { echo "office bundle is missing $1" >&2; exit 1; }; } + +# dpkg -i does not resolve dependencies and the guest is a slim Debian: the +# TDF LibreOffice debs and the .NET SDK both need libraries that are not here +# by default. `apt-get install -f` after each dpkg pulls what they name; ICU +# is installed up front because dotnet refuses to run without it (the setup +# script's alternative, DOTNET_SYSTEM_GLOBALIZATION_INVARIANT, silently +# changes culture behaviour, so give it the real thing instead). apt-get update -apt-get install -y --no-install-recommends \ - pandoc libreoffice-writer libreoffice-calc libreoffice-impress \ - fonts-dejavu fonts-liberation2 -rm -rf /var/lib/apt/lists/* +apt-get install -y --no-install-recommends libicu72 fonts-dejavu fonts-liberation2 -# .NET SDK 8 into /opt/dotnet (the docx/xlsx skills drive OpenXML with it). -case "$ARCH" in - amd64) DOTNET_ARCH=x64 ;; - arm64) DOTNET_ARCH=arm64 ;; -esac -curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh -bash /tmp/dotnet-install.sh --channel 8.0 --install-dir /opt/dotnet --architecture "$DOTNET_ARCH" -rm /tmp/dotnet-install.sh +# --- .NET SDK (required by the docx/xlsx skills) --- +# An 8.x SDK specifically, and this exact 8.x: only SDK 8 carries the net8.0 +# targeting packs locally, and a newer SDK tries to restore them from NuGet, +# which the offline feed does not carry (NU1101). +dotnet_tar="$(bundled 'dotnet-sdk-*-linux-x64.tar.gz')" +require "dotnet-sdk-*-linux-x64.tar.gz" "$dotnet_tar" +DOTNET_VERSION="$(basename "$dotnet_tar" | sed -E 's/^dotnet-sdk-(.+)-linux-x64\.tar\.gz$/\1/')" +mkdir -p /opt/dotnet +if [ "$ARCH" = "amd64" ]; then + tar -xzf "$dotnet_tar" -C /opt/dotnet +else + curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh + bash /tmp/dotnet-install.sh --version "$DOTNET_VERSION" --install-dir /opt/dotnet --architecture arm64 + rm /tmp/dotnet-install.sh +fi ln -sf /opt/dotnet/dotnet /usr/local/bin/dotnet -dotnet --list-sdks - -# Python package set mirrored from the office bundle's wheel list, installed -# into the system python at build time (the guest never runs pip online). -pip3 install --break-system-packages --no-cache-dir \ - pandas openpyxl 'markitdown[pptx]' reportlab pypdf matplotlib - -# Prebuilt pure-JS node module trees from the bundle, merged into one tree on -# NODE_PATH (mirrors the setup script's ~/.node_modules require() fallback). -if compgen -G "$BUNDLE/*-node-modules.zip" >/dev/null; then - mkdir -p /opt/codev-office/node_modules /tmp/modules-staging - for mod_zip in "$BUNDLE"/*-node-modules.zip; do - unzip -q "$mod_zip" -d /tmp/modules-staging/current - if [ -d /tmp/modules-staging/current/node_modules ]; then - cp -R /tmp/modules-staging/current/node_modules/. /opt/codev-office/node_modules/ - fi - rm -rf /tmp/modules-staging/current - done +dotnet --list-sdks | grep -q "^${DOTNET_VERSION} " || { + echo "expected .NET SDK ${DOTNET_VERSION}, got: $(dotnet --list-sdks)" >&2 + exit 1 +} + +# --- pandoc --- +pandoc_deb="$(bundled 'pandoc-*-amd64.deb')" +require "pandoc-*-amd64.deb" "$pandoc_deb" +PANDOC_VERSION="$(basename "$pandoc_deb" | sed -E 's/^pandoc-([0-9.]+)-[0-9]+-amd64\.deb$/\1/')" +if [ "$ARCH" = "amd64" ]; then + pandoc_install="$pandoc_deb" +else + pandoc_install=/tmp/pandoc-arm64.deb + curl -fsSL -o "$pandoc_install" \ + "https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/$(basename "${pandoc_deb%-amd64.deb}-arm64.deb")" fi +dpkg -i "$pandoc_install" || apt-get install -y --no-install-recommends -f +rm -f /tmp/pandoc-arm64.deb +pandoc --version | head -1 | grep -q "${PANDOC_VERSION}" -# Skills into a system path; /etc/skel seeds each fresh sandbox home with the -# config pointing at them (vz homes get skel at image build, bwrap homes are -# seeded by the provider boot script). -if compgen -G "$BUNDLE/*-skill.zip" >/dev/null; then - mkdir -p /opt/codev-skills - for skill_zip in "$BUNDLE"/*-skill.zip; do - name="$(basename "$skill_zip" .zip)" - name="${name%-skill}" - unzip -q "$skill_zip" -d "/opt/codev-skills/$name" +# --- LibreOffice --- +lo_tar="$(bundled 'LibreOffice_*_Linux_x86-64_deb.tar.gz')" +require "LibreOffice_*_Linux_x86-64_deb.tar.gz" "$lo_tar" +LO_VERSION="$(basename "$lo_tar" | sed -E 's/^LibreOffice_(.+)_Linux_x86-64_deb\.tar\.gz$/\1/')" +if [ "$ARCH" = "amd64" ]; then + lo_install="$lo_tar" +else + lo_install=/tmp/libreoffice-aarch64.tar.gz + curl -fsSL -o "$lo_install" \ + "https://download.documentfoundation.org/libreoffice/stable/${LO_VERSION}/deb/aarch64/LibreOffice_${LO_VERSION}_Linux_aarch64_deb.tar.gz" +fi +lo_tmp="$(mktemp -d)" +tar -xzf "$lo_install" -C "$lo_tmp" +dpkg -i "$lo_tmp"/LibreOffice_*/DEBS/*.deb || apt-get install -y --no-install-recommends -f +rm -rf "$lo_tmp" /tmp/libreoffice-aarch64.tar.gz +# The TDF debs declare almost no dependencies, so `dpkg -i` succeeds and the +# `-f` fallback above never fires — while LibreOffice still needs a pile of +# system libraries a slim Debian does not carry. Hand-listing them does not +# work: it dlopens several (libnss3's libssl3.so, libcairo.so.2), so they are +# invisible to ldd and surface only as "error while loading shared libraries" +# the first time soffice runs. Debian packages the same software, so its +# libreoffice-core dependencies are the authoritative list — take those and +# let apt resolve them, rather than maintaining a guess here. +apt-get install -y --no-install-recommends $( + apt-cache depends --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances \ + libreoffice-core | awk '/Depends:/ {print $2}' | grep -vE '^(libreoffice|ure|<)' | sort -u +) +# The debs only create a versioned /usr/local/bin/libreofficeX.Y symlink, but +# doc_to_docx.sh (docx skill) resolves LibreOffice solely as `soffice` on PATH. +if ! command -v soffice >/dev/null 2>&1; then + for candidate in /opt/libreoffice*/program/soffice; do + [ -x "$candidate" ] && ln -sf "$candidate" /usr/local/bin/soffice && break done - mkdir -p /etc/skel/.config/codev - cat > /etc/skel/.config/codev/codev.json <<'JSON' +fi +command -v soffice >/dev/null 2>&1 || { echo "soffice is not on PATH after installing LibreOffice" >&2; exit 1; } +# Convert something, rather than just checking the binary is on PATH: a +# LibreOffice missing a shared library resolves fine as a path and fails only +# when the docx skill first tries to use it, which is far too late to find out. +lo_check="$(mktemp -d)" +printf 'sandbox image check' > "$lo_check/probe.txt" +# Guarded so a failing soffice does not trip `set -e` before the check below +# can say what went wrong; its stderr is kept for the same reason. +HOME="$lo_check" soffice --headless --convert-to pdf "$lo_check/probe.txt" --outdir "$lo_check" || true +[ -s "$lo_check/probe.pdf" ] || { echo "LibreOffice ${LO_VERSION} cannot convert a document" >&2; exit 1; } +rm -rf "$lo_check" + +# --- Python packages --- +# amd64 installs the bundle's wheels verbatim and offline. arm64 has no x86_64 +# wheels to install, so it resolves from PyPI under a constraints file built +# from those same wheels: same versions, including transitive ones, arch-correct +# binaries. Guest python is 3.11, so cp311 is the self-consistent selection. +python3 - "$BUNDLE/python-wheels" > /tmp/office-constraints.txt <<'PY' +import os, re, sys + +# Pick, per distribution, the bundle wheel that pip would install on cp311: +# an exact cp311 build first, then a forward-compatible abi3 build (magika +# ships cp39-abi3, protobuf cp310-abi3 — miss these and arm64 silently +# resolves a newer version than the bundle proved), then a pure-python wheel. +def rank(pytag, abitag, plattag): + if "cp311" in pytag: + return 3 + if abitag == "abi3" and re.fullmatch(r"cp(\d+)", pytag) and int(pytag[2:]) <= 311: + return 2 + # abi-less wheels, whether universal (py3-none-any) or platform-tagged + # (magika ships py3-none-manylinux_x86_64). Only the version is taken from + # these, so an x86_64 tag here still yields the right pin for arm64, where + # pip picks that version's own aarch64-compatible wheel. + if abitag == "none" and pytag.startswith("py"): + return 1 + return 0 + +def version_key(version): + return tuple(int(part) if part.isdigit() else 0 for part in re.split(r"[.\-+]", version)) + +best = {} +for name in os.listdir(sys.argv[1]): + if not name.endswith(".whl"): + continue + dist, version, rest = name[:-4].split("-", 2) + tags = rest.split("-") + if len(tags) < 3: + continue + scored = rank(tags[-3], tags[-2], tags[-1]) + if not scored: + continue + key = dist.replace("_", "-").lower() + if key not in best or (scored, version_key(version)) > best[key][0]: + best[key] = ((scored, version_key(version)), version) +for key in sorted(best): + print(f"{key}=={best[key][1]}") +PY +PIP_PACKAGES=(pandas openpyxl 'markitdown[pptx]' reportlab pypdf matplotlib) +if [ "$ARCH" = "amd64" ]; then + pip3 install --break-system-packages --no-cache-dir \ + --no-index --find-links "$BUNDLE/python-wheels" "${PIP_PACKAGES[@]}" +else + pip3 install --break-system-packages --no-cache-dir \ + -c /tmp/office-constraints.txt "${PIP_PACKAGES[@]}" +fi +rm -f /tmp/office-constraints.txt +python3 -c 'import pandas, openpyxl, markitdown, reportlab, pypdf, matplotlib, defusedxml' + +# --- JS module trees --- +# Merged into one tree the way the setup script merges them under +# ~/.codev-office. Reached two ways on purpose: NODE_PATH in the guest env +# (which the server exports, so its shell tool inherits it) and an +# /etc/skel/.node_modules symlink — one of Node's global require() fallback +# folders — so require() also resolves from a plain shell in any directory. +mkdir -p "$OFFICE_HOME/node_modules" +staging="$(mktemp -d)" +for mod_zip in "$BUNDLE"/*-node-modules.zip; do + [ -f "$mod_zip" ] || continue + unzip -q "$mod_zip" -d "$staging/current" + [ -d "$staging/current/node_modules" ] && cp -R "$staging/current/node_modules/." "$OFFICE_HOME/node_modules/" + rm -rf "$staging/current" +done +rm -rf "$staging" +mkdir -p /etc/skel +ln -sfn "$OFFICE_HOME/node_modules" /etc/skel/.node_modules + +# --- Chromium for Playwright (pdf cover rendering) --- +# Playwright launches the browser build matching its own version, so the +# bundle ships the module tree and the browser together. That zip is an +# x86_64 build; on arm64 the bundled module installs its own matching browser. +export PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright +mkdir -p "$PLAYWRIGHT_BROWSERS_PATH" +playwright_cli="$OFFICE_HOME/node_modules/playwright/cli.js" +if [ "$ARCH" = "amd64" ] && [ -f "$BUNDLE/ms-playwright-chromium.zip" ]; then + unzip -q "$BUNDLE/ms-playwright-chromium.zip" -d "$PLAYWRIGHT_BROWSERS_PATH" +elif [ -f "$playwright_cli" ]; then + NODE_PATH="$OFFICE_HOME/node_modules" node "$playwright_cli" install chromium +fi +# The browser binary alone is not enough on a slim Debian: without its shared +# libraries Chromium is present but cannot launch, which surfaces only at +# runtime as a failed pdf cover. Non-fatal like every other playwright step in +# the setup script — the rest of the pdf skill works without covers. +if [ -f "$playwright_cli" ]; then + NODE_PATH="$OFFICE_HOME/node_modules" node "$playwright_cli" install-deps chromium \ + || echo "warn: playwright install-deps failed; pdf cover rendering may be unavailable" >&2 +fi +ls "$PLAYWRIGHT_BROWSERS_PATH"/chromium-* >/dev/null 2>&1 \ + || echo "warn: no chromium in $PLAYWRIGHT_BROWSERS_PATH; pdf cover rendering unavailable" >&2 + +# --- Offline NuGet feed --- +# Required, exactly as in the setup script: the docx skill's CLI restores +# DocumentFormat.OpenXml and friends from here, and a sealed sandbox has no +# route to nuget.org. The config clears nuget.org so a restore fails loudly +# rather than hanging on an unreachable host. +feed_src="$BUNDLE/nuget-offline-feed" +[ -d "$feed_src" ] || { echo "nuget-offline-feed not found in the bundle" >&2; exit 1; } +feed="$OFFICE_HOME/nuget-offline-feed" +mkdir -p "$feed" /etc/skel/.nuget/NuGet +cp "$feed_src"/*.nupkg "$feed/" +cat > /etc/skel/.nuget/NuGet/NuGet.Config < + + + + + + +EOF + +# --- Skills --- +# Into a system path; the skel config below points every fresh home at it. +mkdir -p /opt/codev-skills +for skill_zip in "$BUNDLE"/*-skill.zip; do + [ -f "$skill_zip" ] || continue + name="$(basename "$skill_zip" .zip)" + name="${name%-skill}" + # Unzip through a staging dir and move the inner tree up, exactly as the + # setup script does: the zips carry their own top-level / directory, + # so unzipping straight into the destination buries SKILL.md a level deeper + # than the proven layout. Discovery survives that (its glob is recursive and + # these skills name themselves in frontmatter), but a skill without a + # frontmatter name would silently fail to load, since the filename fallback + # only applies to a SKILL.md sitting directly in the configured directory. + staging="$(mktemp -d)" + unzip -q "$skill_zip" -d "$staging" + rm -rf "/opt/codev-skills/$name" + if [ -d "$staging/$name" ]; then + mv "$staging/$name" "/opt/codev-skills/$name" + else + mv "$staging" "/opt/codev-skills/$name" + fi + rm -rf "$staging" +done +for required in minimax-docx minimax-xlsx; do + [ -f "/opt/codev-skills/$required/SKILL.md" ] \ + || { echo "$required is missing or has no SKILL.md at its root" >&2; exit 1; } +done +mkdir -p /etc/skel/.config/codev +cat > /etc/skel/.config/codev/codev.json <<'JSON' { "skills": { "paths": ["/opt/codev-skills"] } } JSON -fi -# Env the guest boot paths source (vz init and the bwrap boot script). -cat > /etc/codev-guest.env <<'ENV' +# Env the guest boot paths source (vz init and the bwrap/wsl boot scripts). +cat > /etc/codev-guest.env < /etc/codev-office-versions </dev/null || true chown -R codev:codev /home/codev diff --git a/packages/codev-sandbox/script/build-image.ts b/packages/codev-sandbox/script/build-image.ts index ea57677d47..b27a586c4e 100644 --- a/packages/codev-sandbox/script/build-image.ts +++ b/packages/codev-sandbox/script/build-image.ts @@ -177,15 +177,22 @@ function collectArchs(): SandboxArch[] { return [hostArch() === "arm64" ? "arm64" : "x64"] } -// Only the arch-independent parts of the office bundle enter the build -// context: skill zips and prebuilt pure-JS node-module trees. Binaries -// (dotnet, pandoc, wheels) are fetched per-arch inside the build instead. +// The whole office bundle enters the build: install-office.sh installs the +// versions it pins, which means it needs the artifacts themselves (dotnet +// tarball, LibreOffice debs, pandoc deb, wheels, the offline NuGet feed and +// Playwright's chromium), not just the arch-independent zips. Its x86_64 +// binaries are used as-is on amd64; the arm64 build reads the versions off +// these same filenames and fetches those versions arch-correct. +// +// macOS artifacts are dropped: nothing in a Linux guest can use them and they +// are a large share of the bundle. Bind-mounted rather than COPYed by the +// Dockerfile, so none of this lands in an image layer. async function stageOfficeBundle() { - // Clear previous zips only — the dir's tracked .gitignore/.keep stay. + // Clear previous contents only — the dir's tracked .gitignore/.keep stay. const stage = join(imageDir, "office", "bundle") await mkdir(stage, { recursive: true }) for (const name of await readdir(stage)) { - if (name.endsWith(".zip")) await rm(join(stage, name)) + if (name !== ".gitignore" && name !== ".keep") await rm(join(stage, name), { recursive: true, force: true }) } if (!officeBundle) return const scratch = join(outDir, "office-bundle-stage") @@ -194,10 +201,8 @@ async function stageOfficeBundle() { await $`unzip -q -o ${officeBundle} -d ${scratch}` const dirs = await readdir(scratch) const root = dirs.length === 1 ? join(scratch, dirs[0]!) : scratch - const wanted = (await readdir(root)).filter( - (name) => name.endsWith("-skill.zip") || name.endsWith("-node-modules.zip"), - ) - for (const name of wanted) await cp(join(root, name), join(stage, name)) + const wanted = (await readdir(root)).filter((name) => !/macos|osx|\.dmg$/i.test(name)) + for (const name of wanted) await cp(join(root, name), join(stage, name), { recursive: true }) await rm(scratch, { recursive: true, force: true }) console.log(`staged from office bundle: ${wanted.join(", ") || "(nothing matched)"}`) }