Skip to content

refactor: deepen prepare-image architecture - #67

Open
edhor1608 wants to merge 4 commits into
mainfrom
codex/deepen-prepare-image-architecture
Open

refactor: deepen prepare-image architecture#67
edhor1608 wants to merge 4 commits into
mainfrom
codex/deepen-prepare-image-architecture

Conversation

@edhor1608

@edhor1608 edhor1608 commented Jul 26, 2026

Copy link
Copy Markdown
Owner

What changed

  • Consolidated source discovery, staging, collision-safe output allocation, commit, and rollback in the prepare-image workflow.
  • Introduced a deep image-engine module for FFmpeg and ffprobe selection, EXIF orientation, layout, white-canvas rendering, sRGB conversion, and baseline JPEG export.
  • Replaced implementation-coupled unit tests with command-level acceptance coverage for concurrency, rollback, bundled binary fallback, crop and contain behavior, borders, color conversion, and metadata.
  • Aligned CI, formatter, package metadata, architecture guidance, and the decision log; removed stale slice documents and obsolete shallow modules.

Why

The previous workflow spread product invariants across several shallow modules and exposed internal layout helpers as architecture boundaries. That made atomic output behavior, image-engine guarantees, documentation, and tests harder to evolve coherently. The new boundaries group behavior around the two stable responsibilities: preparing outputs and rendering images.

Impact

The public command remains unchanged: prepare-image still accepts PNG, JPEG, TIFF, or directories and writes high-quality baseline sRGB JPEGs. Internally, failure handling and concurrent output allocation are now explicit and covered end to end.

Validation

  • bun install --frozen-lockfile
  • bun run check (TypeScript 7, type-aware Oxlint, Oxfmt, 22 tests / 87 assertions)
  • bun run prepare-image --help
  • git diff --check

Note

Refactor prepare-image pipeline into a dedicated image engine with batch support

  • Extracts all ffmpeg/ffprobe rendering logic into a new image_engine.ts module covering color management, EXIF orientation handling, layout computation (landscape/portrait cover/contain), and sRGB EXIF tagging.
  • Replaces the single-image prepareImage API in prepare_image.ts with prepareImages, which supports file or directory inputs, stages outputs, and commits atomically with collision-safe suffixed filenames.
  • The CLI in prepare_image.ts is simplified to delegate all file/directory logic to prepareImages; it now prints every written output path.
  • Behavioral Change: directory runs are all-or-nothing — on any failure, staged files are removed and already-committed outputs are rolled back. Colliding basenames produce suffixed files (e.g. photo-1.jpg) rather than overwriting.

Macroscope summarized e9269b7.

Summary by CodeRabbit

  • New Features
    • Added multi-image directory processing with staged, all-or-nothing publishing.
    • Outputs are generated as collision-safe .jpg files without overwriting existing files.
    • Added upload-ready rendering with sRGB tagging and source metadata cleanup.
    • Added bundled media-tool fallbacks when system tools are unavailable.
  • Bug Fixes
    • Improved EXIF orientation, color conversion, cropping, and border handling.
  • Documentation
    • Updated usage, output guarantees, prerequisites, and quality-check guidance.
  • CI / Tests
    • Unified validation through bun run check and expanded integration coverage.

Copy link
Copy Markdown
Owner Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR moves image preparation into a staged batch workflow and adds a dedicated image engine. The engine handles probing, orientation, layout, color conversion, rendering, and sRGB metadata. The CLI prints committed paths. Integration tests cover batching, rollback, collisions, rendering, metadata, color conversion, and bundled tools. Bun scripts, CI checks, project guidance, and product documentation are updated.

Possibly related PRs


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Docstring Coverage ❌ Error Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main architectural refactor of the prepare-image workflow and image engine.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/deepen-prepare-image-architecture
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch codex/deepen-prepare-image-architecture

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/domain/image_engine.ts`:
- Around line 518-529: Update getSourceColorProperties to preserve conversion
when only some color tags are available: retain validated primaries and transfer
values, default an unset range to “pc” for still images, and infer “gbr” for RGB
pixel formats when color_space is unknown or absent. Return null only when the
required color information cannot be safely resolved, rather than requiring all
four tags to be explicitly present.
- Around line 225-229: Update the image preparation flow around
inspectSourceDimensions, computePrepareImageLayout, and buildPrepareImageFilter
so EXIF orientations 5–8 are normalized before cropping and scaling, using the
matching orientation transform while preserving the existing rotation handling.
Ensure crop dimensions are based on the transformed frame geometry and avoid
applying swapped dimensions to an unrotated 0:v stream.

In `@src/domain/prepare_image.ts`:
- Around line 84-86: Remove the unreachable empty-string guard from planImages,
since it receives the resolved output path and empty --out values are already
rejected by the CLI directory validation. Keep the existing file-path validation
behavior for resolved paths.

In `@tests/helpers/cli.ts`:
- Around line 15-18: The CLI test helper’s Bun invocation must work with a
stripped PATH. Update the process setup around Bun.spawn to invoke the
prepare-image entrypoint through the absolute process.execPath and ensure the
child environment supplies absolute ffmpeg and ffprobe paths, using the resolved
ffmpeg-static and existing bundled ffprobe symbols while preserving
caller-provided environment overrides.

In `@tests/integration/prepare_image.integration.test.ts`:
- Around line 192-200: The hasExifOrientationTag and hasSrgbColorSpaceTag
helpers must stop scanning the entire JPEG buffer, which can match compressed
image data. Reuse the APP1/Exif segment-walking approach from image_engine.ts:
traverse markers from SOI, extract the Exif payload from APP1 segments, and
search the tag patterns only within that payload.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3480f66b-08fa-4c11-9dc0-0b0a3707cf8d

📥 Commits

Reviewing files that changed from the base of the PR and between f958594 and e9f1364.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc
  • .github/workflows/ci.yml
  • .gitignore
  • .oxfmtrc.json
  • AGENTS.md
  • CONTEXT.md
  • README.md
  • docs/agents/domain.md
  • docs/agents/issue-tracker.md
  • docs/plans/bundle-ffmpeg-binaries.md
  • docs/plans/decisions-log.md
  • docs/plans/prepare-image-directory-input.md
  • docs/plans/typescript-7-migration.md
  • docs/plans/v1-prepare-image-cli.md
  • docs/plans/v1-prepare-image-layout.md
  • docs/plans/v1-product-split-cleanup.md
  • docs/v1_split_plan.md
  • index.ts
  • package.json
  • src/cli/prepare_image.ts
  • src/domain/image_engine.ts
  • src/domain/media_process.ts
  • src/domain/output_path.ts
  • src/domain/prepare_image.ts
  • src/domain/prepare_image_layout.ts
  • tests/helpers/cli.ts
  • tests/integration/prepare_image.integration.test.ts
  • tests/output_path.test.ts
  • tests/prepare_image_layout.test.ts
💤 Files with no reviewable changes (15)
  • tests/output_path.test.ts
  • docs/plans/v1-prepare-image-cli.md
  • docs/plans/v1-product-split-cleanup.md
  • .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc
  • tests/prepare_image_layout.test.ts
  • docs/plans/typescript-7-migration.md
  • docs/plans/v1-prepare-image-layout.md
  • index.ts
  • docs/v1_split_plan.md
  • docs/plans/prepare-image-directory-input.md
  • src/domain/output_path.ts
  • docs/plans/bundle-ffmpeg-binaries.md
  • src/domain/prepare_image_layout.ts
  • src/domain/media_process.ts
  • .gitignore
📜 Review details
🧰 Additional context used
📓 Path-based instructions (7)
docs/agents/issue-tracker.md

📄 CodeRabbit inference engine (AGENTS.md)

Track executable work in Linear Issues/Subissues and consult docs/agents/issue-tracker.md for issue-tracker guidance.

Files:

  • docs/agents/issue-tracker.md
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Run TypeScript typechecking with bunx tsc and maintain type-correct TypeScript code.
Use type-aware Oxlint and Oxfmt verification as part of the quality checks.
The v1 acceptance test must continue to pass, including the command and image-engine acceptance coverage.
Support PNG, JPEG, and TIFF input files and directories containing those image types.
Place each input image on a white canvas and export a high-quality baseline sRGB JPEG suitable for manual Instagram upload.
Preserve atomic output allocation, commit, and rollback behavior in the image-preparation workflow.
Support a --border-px integer option, including zero, and the documented --help command.
Use system ffmpeg and ffprobe when available while maintaining independently working bundled adapters when they are absent from PATH.

Files:

  • tests/helpers/cli.ts
  • src/domain/prepare_image.ts
  • src/domain/image_engine.ts
  • src/cli/prepare_image.ts
  • tests/integration/prepare_image.integration.test.ts
docs/plans/decisions-log.md

📄 CodeRabbit inference engine (AGENTS.md)

docs/plans/decisions-log.md: Record durable architecture or product decisions immediately in docs/plans/decisions-log.md.
Read docs/plans/decisions-log.md before domain-sensitive work.

Files:

  • docs/plans/decisions-log.md
CONTEXT.md

📄 CodeRabbit inference engine (AGENTS.md)

Read CONTEXT.md before domain-sensitive work.

Files:

  • CONTEXT.md
src/domain/prepare_image.ts

📄 CodeRabbit inference engine (AGENTS.md)

Keep source discovery, staging, atomic output allocation, commit, and rollback in src/domain/prepare_image.ts.

Files:

  • src/domain/prepare_image.ts
src/domain/image_engine.ts

📄 CodeRabbit inference engine (AGENTS.md)

Keep FFmpeg/ffprobe adapters, orientation handling, layout, white-canvas rendering, and JPEG export in src/domain/image_engine.ts.

Files:

  • src/domain/image_engine.ts
src/cli/prepare_image.ts

📄 CodeRabbit inference engine (AGENTS.md)

Keep argv parsing and stdout/stderr adaptation in src/cli/prepare_image.ts.

Files:

  • src/cli/prepare_image.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: CR
Repo: edhor1608/passepartout

Timestamp: 2026-07-26T08:39:46.672Z
Learning: Use `bun install --frozen-lockfile` for installation and `bun run check` before pull requests.
Learnt from: CR
Repo: edhor1608/passepartout

Timestamp: 2026-07-26T08:39:46.672Z
Learning: Keep executable work in Linear and prefer explicit code plus local comments over new slice Markdown.
📚 Learning: 2026-05-21T12:23:11.207Z
Learnt from: edhor1608
Repo: edhor1608/passepartout PR: 63
File: docs/agents/domain.md:34-34
Timestamp: 2026-05-21T12:23:11.207Z
Learning: In this repository (edhor1608/passepartout), the internal agent skill `grill-with-docs` is already established and understood by agents in the codebase. Code review should not require adding redundant explanations or links to `grill-with-docs` in agent-facing docs (e.g., `docs/agents/**/*.md`); only document it if there’s a concrete reason (e.g., introducing a new behavior or dependency that isn’t already understood).

Applied to files:

  • docs/agents/domain.md
  • docs/agents/issue-tracker.md
🪛 LanguageTool
AGENTS.md

[grammar] ~41-~41: Use a hyphen to join words.
Context: ...very acceptance test with an explicit 60 second per-test timeout. - `bun run test...

(QB_NEW_EN_HYPHEN)

🔇 Additional comments (14)
src/domain/image_engine.ts (1)

283-324: LGTM!

Also applies to: 540-605

src/domain/prepare_image.ts (1)

33-75: LGTM!

Also applies to: 94-108, 132-147

src/cli/prepare_image.ts (1)

1-1: LGTM!

Also applies to: 75-78

tests/integration/prepare_image.integration.test.ts (1)

56-108: LGTM!

Also applies to: 287-353, 418-462, 499-583, 644-679

package.json (2)

9-10: LGTM!

Also applies to: 12-18


8-8: 📐 Maintainability & Code Quality

Verify the type-aware Oxlint quality-gate contract at the package script.

The executable command must enable type-aware analysis before the documentation and durable verification record make that claim.

  • package.json#L8-L8: verify the supported Oxlint configuration/CLI option and enable type-aware mode if absent.
  • AGENTS.md#L27-L28: retain the type-aware quality-gate claim only after the package command/configuration is confirmed.
  • README.md#L72-L73: keep the user-facing quality-gate description aligned with the actual check.
  • docs/plans/decisions-log.md#L1060-L1061: record the verification result only after the same gate is proven.

As per coding guidelines, quality checks must use type-aware Oxlint and Oxfmt verification.

Source: Coding guidelines

.github/workflows/ci.yml (1)

27-28: LGTM!

.oxfmtrc.json (1)

10-11: LGTM!

AGENTS.md (1)

15-15: LGTM!

Also applies to: 41-49, 50-53, 54-66

CONTEXT.md (1)

7-35: LGTM!

Also applies to: 47-47, 67-75

README.md (1)

15-21: LGTM!

Also applies to: 60-61, 75-85

docs/agents/domain.md (1)

12-14: LGTM!

Also applies to: 23-24

docs/agents/issue-tracker.md (1)

9-9: LGTM!

Also applies to: 27-28, 37-37

docs/plans/decisions-log.md (1)

947-1055: LGTM!

Also applies to: 1057-1059, 1062-1068

Comment on lines +225 to +229
if (rotation === 90 || rotation === 270 || isExifOrientationSwapped(readExifOrientation(path))) {
return { color, height: width, width: height };
}

return { color, height, width };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for any rotation handling in the filter chain and for orientation coverage in tests.
rg -nP 'transpose|autorotate|hflip|vflip|orientation' src tests
# Inspect the exif-orientation integration test to see which orientation value is exercised.
rg -nP -B10 -A30 'insertExifOrientation\(' tests/integration/prepare_image.integration.test.ts

Repository: edhor1608/passepartout

Length of output: 3930


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== image_engine outline =="
ast-grep outline src/domain/image_engine.ts --match image --view expanded || true
echo "== relevant image_engine sections =="
sed -n '200,360p' src/domain/image_engine.ts
echo "== render methods =="
rg -n "renderReadyToUploadImage|inspectSourceDimensions|computePrepareImageLayout|ffmpeg|filters|transpose|exif_orientation|hflip|vflip|scale|crop" src/domain/image_engine.ts
echo "== integration createFixture and pixel probe area =="
sed -n '1,210p' tests/integration/prepare_image.integration.test.ts
sed -n '570,610p' tests/integration/prepare_image.integration.test.ts

Repository: edhor1608/passepartout

Length of output: 11699


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== image_engine render/layout implementation =="
sed -n '120,175p' src/domain/image_engine.ts
sed -n '330,530p' src/domain/image_engine.ts

echo "== all ffmpeg filter usage =="
rg -n "filter|transpose|exif_orientation|format=|setsar|scale=|crop=" src/domain/image_engine.ts

echo "== deterministic behavior model for orientation mismatch =="
python3 - <<'PY'
from dataclasses import dataclass

`@dataclass`
class Source:
    probe_width: int
    probe_height: int
`@dataclass`
class Layout:
    inner_width:  int
    inner_height: int
    crop_w: int
    crop_h: int
    crop_x: int
    crop_y: int
    crop_expr: str

def build_layout(probe_width, probe_height):
    # mimic landscape branch for small border and enough source pixels
    target_max = 1080
    ratio_w = 4
    ratio_h = 5
    for output_width in range(target_max, ratio_w - 1, -ratio_w):
        output_height = (output_width / ratio_w) * ratio_h
        if output_height > target_max:
            continue
        inner_width = output_width
        inner_height = output_height
        if inner_width <= probe_width or inner_height <= probe_height:
            crop_w = min(inner_width, probe_width)
            crop_h = min(inner_height, probe_height)
            crop_x = max(0, (probe_width - crop_w)//2)
            crop_y = max(0, (probe_height - crop_h)//2)
            return Layout(inner_width, inner_height, crop_w, crop_h, crop_x, crop_y, f"{crop_w}:{crop_h}:{crop_x}:{crop_y}")
    return None

cases = [
    ("unswapped 40x60 probe/source", 40, 60),
    ("swapped EXIF 6: probe thinks 60x40, frame is still 40x60", 60, 40),
]
for name, sw, sh in cases:
    layout = build_layout(sw, sh)
    invalid_crop = layout.crop_w > sh or layout.crop_h > sw
    print(f"{name}: layout crop={layout.crop_expr}, inner={layout.inner_width}x{layout.inner_height}, invalid_crop={invalid_crop}")
PY

echo "== image fixtures around created images =="
rg -n "raw-orientation|oriented|createFixture|createHorizontalPatternFixture" tests src -g '*.jpg' -g '*.jpeg' || true

Repository: edhor1608/passepartout

Length of output: 8031


Rotate EXIF swap orientations before cropping the frame.

inspectSourceDimensions returns swapped width/height for EXIF 5–8, which makes computePrepareImageLayout choose landscape layouts and crop=W:H values based on swapped frame geometry. buildPrepareImageFilter then crops the still-unrotated 0:v; orientations 6 and 7 can produce crop dimensions larger than the real frame and ffmpeg fails, while 5 and 8 crop the wrong area before scaling. Apply the matching transpose/hflip/vflip/exif_orientation filter before the crop/scale chain, or stop swapping these dimensions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/domain/image_engine.ts` around lines 225 - 229, Update the image
preparation flow around inspectSourceDimensions, computePrepareImageLayout, and
buildPrepareImageFilter so EXIF orientations 5–8 are normalized before cropping
and scaling, using the matching orientation transform while preserving the
existing rotation handling. Ensure crop dimensions are based on the transformed
frame geometry and avoid applying swapped dimensions to an unrotated 0:v stream.

Comment on lines +518 to +529
function getSourceColorProperties(stream: Record<string, unknown>): SourceColorProperties | null {
const space = getSupportedString(stream, "color_space", SUPPORTED_COLOR_SPACES);
const primaries = getSupportedString(stream, "color_primaries", SUPPORTED_COLOR_PRIMARIES);
const transfer = getSupportedString(stream, "color_transfer", SUPPORTED_COLOR_TRANSFERS);
const range = getSupportedString(stream, "color_range", SUPPORTED_COLOR_RANGES);

if (!space || !primaries || !transfer || !range) {
return null;
}

return { primaries, range, space, transfer };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift

All-or-nothing color handling silently skips conversion for partially tagged sources.

getSourceColorProperties returns null unless all four tags are present and in the allowlist. Still images frequently carry color_primaries/color_transfer while color_space is unknown (RGB PNG/TIFF) or color_range is unset — in those cases wide-gamut input is passed through untouched and exported as if it were sRGB. Consider defaulting missing components (e.g. rangepc for still images, spacegbr for RGB pix_fmts) instead of dropping the whole conversion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/domain/image_engine.ts` around lines 518 - 529, Update
getSourceColorProperties to preserve conversion when only some color tags are
available: retain validated primaries and transfer values, default an unset
range to “pc” for still images, and infer “gbr” for RGB pixel formats when
color_space is unknown or absent. Return null only when the required color
information cannot be safely resolved, rather than requiring all four tags to be
explicitly present.

Comment thread src/domain/prepare_image.ts Outdated
Comment thread tests/helpers/cli.ts Outdated
Comment on lines +15 to +18
const proc = Bun.spawn({
cmd: ["bun", "src/cli/prepare_image.ts", ...args],
cmd: [process.execPath, "run", "--silent", "prepare-image", ...args],
cwd: REPO_ROOT,
env: options?.env ? { ...process.env, ...options.env } : undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the prepare-image script definition and any shell wrappers it relies on.
fd -H -t f 'package.json' -d 2 --exec jq '.scripts'

Repository: edhor1608/passepartout

Length of output: 595


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files of interest =="
git ls-files | rg '(^package\.json$|tests/helpers/cli\.ts$|src/cli/prepare_image\.ts$|package-lock\.json$|bun\.lockb$)' || true

echo
echo "== package.json scripts and deps =="
if [ -f package.json ]; then
  jq '{scripts, packages, dependencies, devDependencies}' package.json
fi

echo
echo "== tests/helpers/cli.ts =="
cat -n tests/helpers/cli.ts

echo
echo "== prepare-image related test usage =="
rg -n --hidden --glob '!node_modules' --glob '!dist' 'prepare-image|PATH|process\.execPath|spawn' tests package.json . -g '!node_modules' || true

Repository: edhor1608/passepartout

Length of output: 7848


🌐 Web query:

Bun npm package scripts run command PATH lookup bun run execute bun binary PATH

💡 Result:

When you execute a command using bun run, Bun modifies the PATH for that specific process to ensure local project binaries are available [1][2]. Specifically, Bun injects the appropriate node_modules/.bin directories (including recursive lookups for nested projects) into the PATH environment variable for the duration of the script's execution [2]. The resolution order for bun run is as follows [3][4][5]: 1. package.json scripts: If a script with the specified name exists in your package.json, it is executed [3][4]. 2. Source files: If a matching file is found (e.g., index.js), Bun attempts to run it [3][4]. 3. Binaries from project packages: Bun checks for binaries located in node_modules/.bin [3][4]. 4. System commands: Finally, for bun run only, Bun falls back to checking the system PATH [3][4]. When executing these commands, Bun does not necessarily require the bun binary itself to be in the PATH to function correctly, as it manages the environment and path lookups internally [2]. However, Bun performs these actions by spawning a subshell [3][6]. On Linux and macOS, it selects from bash, sh, or zsh, while on Windows, it uses the integrated Bun Shell [3][6]. It is important to note that global binaries installed via bun add -g or bun install -g require the Bun installation directory (e.g., ~/.bun/bin) to be manually added to your system PATH for them to be executable globally from a standard shell [7]. If you encounter issues where scripts cannot find installed binaries, ensuring that the global bin directory is present in your shell's PATH is the standard solution [7].

Citations:


🌐 Web query:

Bun source BunPackageScriptRunner exec PATH script commands bun run script "execve"

💡 Result:

Bun’s mechanism for executing scripts via bun run involves several layers of path resolution and process spawning, designed to prioritize package-defined scripts, local binaries, and system commands [1][2][3]. Process Spawning Mechanism At the core, Bun uses posix_spawn (and its extensions) on POSIX platforms to avoid the overhead of fork() and exec() [4]. When executing commands, Bun performs manual resolution for the command path and argv[0] before the system call to ensure security (preventing null-byte injection) and to support its custom resolution logic [4]. Path Resolution and Execution 1. Resolution Order: When bun run <name> is invoked, Bun follows a specific priority: - Package.json scripts: Defined scripts are executed first [1][2][3]. - Source files: Files (e.g., index.js) [1][2][3]. - Binaries in node_modules/.bin: Local project binaries [1][2][3]. - System commands: Resolved via the system PATH (applicable to bun run) [1][2][3]. 2. Subshell Execution: For scripts defined in package.json, Bun executes the command in a subshell [5][2]. On Linux and macOS, it sequentially checks for bash, sh, and zsh, using the first one found [5][3]. On Windows, Bun uses its internal "Bun Shell" to provide cross-platform, bash-like syntax support [5][2]. 3. ENOEXEC Handling: When an attempted execution returns ENOEXEC (commonly occurring with scripts missing a shebang), Bun mimics Node.js/libuv behavior by retrying the execution by wrapping the command in a shell (e.g., /bin/sh <script>), ensuring compatibility for non-executable script files [6]. 4. Environment Manipulation: Bun may modify the PATH environment variable during execution—for example, when using the --bun flag or during lifecycle script execution—to ensure that nested Bun processes or internal shims are correctly prioritized [7][8]. Specifically, when running scripts or utilizing shims, Bun may prepend directories (such as BUN_NODE_DIR) to PATH to ensure the correct Bun binary is invoked [7]. Key Source Code References - Process spawning logic and POSIX integration are found in src/spawn_sys/spawn_process.rs and src/bun.js/api/bun/spawn.zig [4][6][9]. - Lifecycle script management, including shell bin selection and environment setup, is handled in src/install/lifecycle_script_runner.zig [10]. - Command resolution logic for bun run is implemented in src/cli/run_command.zig [8].

Citations:


🌐 Web query:

site:github.com/oven-sh/bun filter_run.rs PATH .exec() shell subprocess bun run package script

💡 Result:

The file src/runtime/cli/filter_run.rs is a component of the Bun runtime codebase responsible for managing and executing concurrent tasks, specifically when using flags like --filter with bun run [1][2][3]. In this context,.exec (or the internal execution logic within this file) handles spawning shell subprocesses to run package scripts defined in a project's configuration [1][4]. The implementation manages process creation, environment configuration, and shell-specific commands [1][4]. Key technical details include: 1. Shell Invocation: When executing commands, it constructs an argument vector (argv) that uses the system shell to run the task [1][4]. On Unix systems, this typically involves invoking the shell with the -c flag to execute the command string, while on other platforms, it may use exec or platform-specific mechanisms [1][4]. 2. Environment Configuration: The runner ensures that the PATH environment variable is correctly configured for the subprocess [1][4]. It uses a scoped mechanism to temporarily modify the PATH for the execution of the package script and restores the original environment afterward, ensuring that local package binaries are correctly resolved [1][4]. 3. Execution Mechanism: The logic utilizes a spawn_process function that takes the configured options, the command arguments, and the environment pointers to initiate the subprocess [1][4]. This is part of a larger system that facilitates the concurrent execution and display of output for package scripts [5][3]. This code is part of the core implementation for Bun's command-line interface, specifically handling the orchestration of parallel or filtered command execution within a workspace or project [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== prepare source =="
sed -n '1,220p' src/cli/prepare_image.ts

echo
echo "== image-engine shell/fallback setup =="
sed -n '1,260p' src/domain/image_engine.ts

Repository: edhor1608/passepartout

Length of output: 7873


Use absolute runtime binaries when running prepare-image in stripped-PATH tests.

prepare-image maps to bun run src/cli/prepare_image.ts, and that script later resolves ffmpeg/ffprobe before falling back to bundled paths. With PATH: dirname(process.execPath), the top-level prepare-image script cannot be invoked via bun run, and command lookups must use absolute paths such as process.execPath, the resolved ffmpeg-static path, and an existing bundled ffprobe path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/helpers/cli.ts` around lines 15 - 18, The CLI test helper’s Bun
invocation must work with a stripped PATH. Update the process setup around
Bun.spawn to invoke the prepare-image entrypoint through the absolute
process.execPath and ensure the child environment supplies absolute ffmpeg and
ffprobe paths, using the resolved ffmpeg-static and existing bundled ffprobe
symbols while preserving caller-provided environment overrides.

Comment on lines +192 to +200
function hasExifOrientationTag(path: string): boolean {
return readFileSync(path).includes(Buffer.from([0x01, 0x12, 0x00, 0x03]));
}

function hasSrgbColorSpaceTag(path: string): boolean {
return readFileSync(path).includes(
Buffer.from([0xa0, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01]),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scanning the whole JPEG for tag byte patterns can produce false positives in entropy-coded data.

Both helpers search the entire file, including compressed scan data, so an incidental byte match makes hasExifOrientationTag(...) === false on Line 621 flaky. Restrict the search to the APP1 segment (walk 0xFF 0xE1 markers from SOI and slice Exif\0\0 payload) rather than the full buffer — the file already has that walking logic in src/domain/image_engine.ts to model it on.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/prepare_image.integration.test.ts` around lines 192 - 200,
The hasExifOrientationTag and hasSrgbColorSpaceTag helpers must stop scanning
the entire JPEG buffer, which can match compressed image data. Reuse the
APP1/Exif segment-walking approach from image_engine.ts: traverse markers from
SOI, extract the Exif payload from APP1 segments, and search the tag patterns
only within that payload.

Orientation tests only checked output dimensions, which cannot distinguish a
correct rotation from a wrong one -- EXIF 5-8 all swap width and height. Adds a
quadrant-marker fixture and per-corner assertions for orientations 5, 6, 7 and 8,
with expectations derived from the EXIF spec rather than from an ffmpeg reference
run. Verified by mutation: removing the dimension swap in inspectSourceDimensions
fails all five orientation tests.

Also removes the unreachable empty-path guard in planImages (callers pass an
already-resolved absolute path; the CLI rejects an empty --out while parsing
argv) and caps the collision-suffix search in commitWithoutOverwrite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFTLY9M1HfSALwHBMTEyPi
@edhor1608
edhor1608 marked this pull request as ready for review July 27, 2026 11:14
@edhor1608

Copy link
Copy Markdown
Owner Author

Addressed review feedback in the latest commit.

EXIF orientation 5–8 crop (major, image_engine.ts) — false positive, not changed.
ffmpeg applies EXIF orientation to the input stream before the filtergraph, including with -filter_complex. Verified empirically on both the system ffmpeg 8.0 and the bundled ffmpeg-static 6.0: for a stored 40x60 JPEG with orientation 5, 6, 7 or 8, [0:v]null[out] yields a 60x40 frame. So the swap in inspectSourceDimensions matches the geometry the crop actually sees; there is no oversized crop and no wrong crop region.

Rather than leave that on an argument, the test now proves it. applies EXIF orientation visually before choosing the output ratio only asserted output dimensions, which cannot distinguish a correct rotation from a wrong one — every orientation in 5–8 swaps width and height. Added a quadrant-marker fixture plus per-corner assertions for orientations 5, 6, 7 and 8, with expectations derived from the EXIF spec (e.g. orientation 5 = transpose across the main diagonal) rather than from an ffmpeg reference run, so the test is not circular. Mutation-checked: removing the dimension swap fails all five orientation tests.

Unreachable guard (major, prepare_image.ts) — fixed. Removed, with a comment recording why the check does not belong there: callers pass an already-resolved absolute path, and the CLI rejects an empty --out during argv parsing.

Partial color tags (trivial, image_engine.ts) — intentional, not changed. Only tagging output as sRGB when all four source components are known is the deliberate conservative choice; inferring range=pc / space=gbr would mean guessing at the source, and guessing wrong silently produces a mis-converted image. Worth revisiting with real wide-gamut fixtures, not as part of this refactor.

Also capped the collision-suffix search in commitWithoutOverwrite so a pathological output directory raises an error instead of looping forever.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/integration/prepare_image.integration.test.ts`:
- Around line 690-744: Add an explicit 60-second timeout to each generated
orientation test in the loop over ORIENTATION_CORNER_EXPECTATIONS by supplying
the timeout argument to test(). Keep the existing test body and expectations
unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 80ced43c-0a87-45b2-9d2e-c764f8b6f412

📥 Commits

Reviewing files that changed from the base of the PR and between e9f1364 and 2d90d32.

📒 Files selected for processing (2)
  • src/domain/prepare_image.ts
  • tests/integration/prepare_image.integration.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript with Bun and verify it through bunx tsc.
Run type-aware Oxlint and Oxfmt verification as part of the quality checks.

Files:

  • tests/integration/prepare_image.integration.test.ts
  • src/domain/prepare_image.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Acceptance tests must use an explicit 60-second per-test timeout; integration tests cover the v1 command and image engine.

Files:

  • tests/integration/prepare_image.integration.test.ts
**/*

📄 CodeRabbit inference engine (AGENTS.md)

**/*: The production command must accept PNG, JPEG, and TIFF images or directories of those images, place each image on a white canvas, and export high-quality baseline sRGB JPEGs.
Output allocation and commits must be atomic, with rollback support for failed image preparation.

Files:

  • tests/integration/prepare_image.integration.test.ts
  • src/domain/prepare_image.ts
src/domain/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Keep image-preparation workflow logic, including source discovery, staging, atomic output allocation, commit, and rollback, in the domain layer; keep concrete image-engine behavior in the image engine module.

Files:

  • src/domain/prepare_image.ts
🔇 Additional comments (2)
src/domain/prepare_image.ts (1)

33-35: LGTM!

Also applies to: 87-89, 134-157

tests/integration/prepare_image.integration.test.ts (1)

56-142: LGTM!

Comment thread tests/integration/prepare_image.integration.test.ts
AGENTS.md requires an explicit 60 second per-test timeout for acceptance tests,
but the file capped every test at 30s. `setDefaultTimeout` overrides the runner's
`--timeout` flag, so the `bun test --timeout 60000` in the test script never took
effect -- verified with a minimal repro.

Fixing it at the file level covers all 26 tests uniformly; adding a third `test()`
argument would have applied the limit to 5 of them and left the file inconsistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFTLY9M1HfSALwHBMTEyPi
@edhor1608

Copy link
Copy Markdown
Owner Author

Fixed, but one level up from the suggestion.

The diagnosis was off: these tests never ran under Bun's 5s default. Line 16 called setDefaultTimeout(30_000), and setDefaultTimeout overrides the runner's --timeout flag — verified with a minimal repro (a test with setDefaultTimeout(1_000) times out after 1s despite bun test --timeout 60000).

The finding still stands, just bigger than reported: the bun test --timeout 60000 in the test script was dead, so all 26 acceptance tests were capped at 30s while AGENTS.md mandates 60s — not only the 5 new ones.

So instead of adding a third test() argument, setDefaultTimeout is now 60_000. That covers every test uniformly. The suggested patch would have put an explicit 60s on 5 of 26 tests and left the other 21 silently at 30s, which is the inconsistency that caused this in the first place — none of the pre-existing tests use a per-test argument.

bun run check green: 26 tests, 99 assertions.

main enabled `exactOptionalPropertyTypes` (#68) plus
`typescript/strict-boolean-expressions` and `typescript/no-non-null-assertion`
(#69) while this PR was open. Both sides had rewritten
`src/cli/prepare_image.ts`, but not as competing implementations: main adapted
the old architecture's expressions to the new rules, while this branch had
already moved directory enumeration and output allocation into the domain behind
`prepareImages()`. The resolution keeps this branch's thin CLI and takes only
main's strict idioms.

The new rules also reach code outside the conflict:

- `image_engine.ts`: three truthiness checks made explicit. All are equivalent --
  `getSupportedString` returns an allowlisted value or null and never "", and
  `sourceCrop` is an object or undefined.
- `prepare_image.ts`: `?.isDirectory()` / `?.isFile()` compared against `true`,
  since optional chaining yields `boolean | undefined`.
- `tests/helpers/cli.ts`: `env: undefined` is no longer assignable to an optional
  property, so the key is now omitted entirely when no override is passed.

No behavior change; all 26 acceptance tests pass unmodified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BFTLY9M1HfSALwHBMTEyPi
Entire-Checkpoint: 4448d79cc6d0

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/cli/prepare_image.ts (1)

35-39: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject unsafe --border-px values.

Number.isInteger accepts values above Number.MAX_SAFE_INTEGER, so a decimal input can be accepted as a rounded pixel value. Use Number.isSafeInteger(parsedBorderPx) to reject values that JavaScript cannot represent exactly.

Proposed fix
-      if (!/^\d+$/.test(value) || !Number.isInteger(parsedBorderPx)) {
+      if (!/^\d+$/.test(value) || !Number.isSafeInteger(parsedBorderPx)) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/prepare_image.ts` around lines 35 - 39, Update the validation in the
--border-px parsing flow to use Number.isSafeInteger(parsedBorderPx) instead of
Number.isInteger, while preserving the existing digit-format check and
invalid-value error behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/cli/prepare_image.ts`:
- Around line 35-39: Update the validation in the --border-px parsing flow to
use Number.isSafeInteger(parsedBorderPx) instead of Number.isInteger, while
preserving the existing digit-format check and invalid-value error behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 188eafca-c1ef-4e93-ba22-fd0fee1a5c6b

📥 Commits

Reviewing files that changed from the base of the PR and between 2d90d32 and e9269b7.

📒 Files selected for processing (5)
  • src/cli/prepare_image.ts
  • src/domain/image_engine.ts
  • src/domain/prepare_image.ts
  • tests/helpers/cli.ts
  • tests/integration/prepare_image.integration.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Maintain TypeScript correctness and pass typechecking with bunx tsc.
Use type-aware Oxlint-compatible TypeScript code and pass the repository lint check.
Keep formatting compliant with Oxfmt.
Image processing must support PNG, JPEG, and TIFF inputs, place images on a white canvas, and export high-quality baseline sRGB JPEGs.

Files:

  • tests/helpers/cli.ts
  • src/cli/prepare_image.ts
  • src/domain/image_engine.ts
  • src/domain/prepare_image.ts
  • tests/integration/prepare_image.integration.test.ts
src/cli/prepare_image.ts

📄 CodeRabbit inference engine (AGENTS.md)

Keep argv parsing and stdout/stderr interaction in src/cli/prepare_image.ts; treat it as the CLI adapter.

Files:

  • src/cli/prepare_image.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx}: Use explicit code and local comments rather than adding new slice Markdown for executable work.
FFmpeg and ffprobe adapters must work with preferred system binaries and independently with bundled fallback binaries when the tools are absent from PATH.

Files:

  • src/cli/prepare_image.ts
  • src/domain/image_engine.ts
  • src/domain/prepare_image.ts
src/domain/image_engine.ts

📄 CodeRabbit inference engine (AGENTS.md)

Keep FFmpeg/ffprobe adapters, orientation handling, layout, white-canvas rendering, and JPEG export in src/domain/image_engine.ts.

Files:

  • src/domain/image_engine.ts
src/domain/prepare_image.ts

📄 CodeRabbit inference engine (AGENTS.md)

Keep source discovery, staging, atomic output allocation, commit, and rollback in src/domain/prepare_image.ts.

Files:

  • src/domain/prepare_image.ts
**/*.{test,spec}.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Acceptance tests must enforce the v1 command behavior, use explicit 60-second per-test timeouts, and cover the command and image-engine integration paths.

Files:

  • tests/integration/prepare_image.integration.test.ts
🔇 Additional comments (4)
src/domain/image_engine.ts (1)

490-504: LGTM!

Also applies to: 524-528

src/domain/prepare_image.ts (1)

84-93: LGTM!

tests/helpers/cli.ts (1)

15-24: LGTM!

tests/integration/prepare_image.integration.test.ts (1)

16-19: LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant