Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
with:
node-version: "24"
- run: bun install --frozen-lockfile
# Screenshot e2e tests drive a real browser
- run: bunx playwright install --with-deps chromium
- run: bun run check
- run: bun prettier --check .
- run: bun test tests/unit/ tests/e2e/
Expand All @@ -38,6 +40,11 @@ jobs:
npm init -y > /dev/null
npm install "$RUNNER_TEMP"/srcpack-*.tgz > /dev/null
echo 'export const x = 1;' > src/index.ts
printf 'import { defineConfig } from "srcpack";\nexport default defineConfig({ bundles: { app: "src/**/*" } });\n' > srcpack.config.mts
printf 'import { defineConfig } from "srcpack";\nexport default defineConfig({ bundles: { app: "src/**/*", home: { screenshot: "http://localhost:9/", onDemand: true } } });\n' > srcpack.config.mts
./node_modules/.bin/srcpack --dry-run | tee out.txt
grep -q 'src/index.ts' out.txt
# Playwright is an optional peer, so it is absent here: a preview
# needs no browser, and a capture explains how to install one
./node_modules/.bin/srcpack home --dry-run | grep -q 'home-NN.png'
if ./node_modules/.bin/srcpack home 2> err.txt; then exit 1; fi
grep -q 'screenshots need Playwright' err.txt
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Zero-config CLI for bundling code into LLM-optimized context files.

```bash
npx srcpack init # Create config interactively
npx srcpack # Bundle all
npx srcpack # Bundle all except on-demand
```

## Why
Expand Down Expand Up @@ -57,7 +57,7 @@ Or add to `package.json`:
| `bundles` | — | Named bundle definitions |
| `upload` | — | Upload destination(s) |

\*Only the default `.srcpack` is emptied automatically — it's srcpack's directory by convention. Any other `outDir` needs an explicit `emptyOutDir: true`, so `outDir: "src"` can't quietly delete your sources. Emptying also happens only on a full run, so `npx srcpack web` leaves other bundles in place.
\*Only the default `.srcpack` is emptied automatically — it's srcpack's directory by convention. Any other `outDir` needs an explicit `emptyOutDir: true`, so `outDir: "src"` can't quietly delete your sources. Named runs (`npx srcpack web`) never empty it, so other bundles stay in place.

### Bundle Config

Expand All @@ -78,16 +78,20 @@ Or add to `package.json`:
{
include: "src/**/*",
linear: { team: "ENG" }, // Linear issues as virtual files
screenshot: "http://localhost:5173", // rendered page as numbered PNGs
outfile: "~/Downloads/bundle.txt", // custom output path
index: true, // include index header (default)
prompt: "./prompts/review.md" // prepend from file (or inline text)
prompt: "./prompts/review.md", // prepend from file (or inline text)
onDemand: true // build only when named
}
```

Patterns follow glob syntax. Prefix with `!` to exclude, `+` to force-include (bypasses `.gitignore`). Binary files are excluded.

A pattern can also name a set of changed files: `git:staged`, `git:unstaged`, `git:untracked`, `git:dirty`, or `git:<rev>` (e.g. `git:main`, `git:HEAD~3`). Deleted files are skipped, and `git:<rev>` compares against the merge base so a stale branch still reports only your own changes. See [Git sources](https://kriasoft.com/srcpack/configuration#git-sources-git-prefix).

A full run skips bundles marked `onDemand: true`; name them explicitly to build them. Emptying `outDir` still removes their previous output there. See [On-Demand Bundles](https://kriasoft.com/srcpack/configuration#on-demand-bundles).

### Linear Issues

A bundle can include [Linear](https://linear.app) issues next to your code. Each issue becomes a virtual file at `linear/issues/ENG-123.md`, so it gets its own index entry and line range — letting you ask whether `[4] src/board.ts` actually implements `[2] ENG-123`.
Expand All @@ -104,6 +108,18 @@ bundles: {

Authentication reads `LINEAR_API_KEY` from the environment (Linear → Settings → Security & access → Personal API keys), never from the config file. `team` is required, completed/canceled/duplicate issues are excluded by default, and issues obey `!` exclusions like any other entry. See [Linear issues](https://kriasoft.com/srcpack/configuration#linear-issues).

### Screenshots

A bundle can capture a rendered page as PNGs, with overlapping detail slices for tall pages. Srcpack scrolls to load lazy content before capture and hides Astro and Nuxt dev toolbars.

```typescript
bundles: {
home: { screenshot: "http://localhost:5173/", onDemand: true },
}
```

`npx srcpack home` writes PNGs to `.srcpack`: `home-00.png` is the whole page when available; tall pages also get `home-01.png`, `home-02.png`, … detail slices. Attach them in filename order. Images stay local, even with upload configured. For a one-off, no config is needed: `npx srcpack --screenshot localhost:5173 --viewport mobile`. See [Screenshots](https://kriasoft.com/srcpack/configuration#screenshots) for Playwright and browser setup.

### Google Drive Upload

To upload bundles to Google Drive, add OAuth credentials to your config:
Expand Down Expand Up @@ -154,14 +170,15 @@ import { utils } from "./utils";
## CLI

```bash
npx srcpack # Bundle all, upload if configured
npx srcpack # Bundle all except on-demand, upload if configured
npx srcpack web api # Bundle specific bundles only
npx srcpack --staged # Bundle staged changes (no config needed)
npx srcpack --dirty # Bundle staged + unstaged + untracked
npx srcpack --since main # Bundle changes since main
npx srcpack --screenshot localhost:5173 # Capture a page as PNGs
npx srcpack --dry-run # Preview without writing files
npx srcpack --emptyOutDir # Empty output directory before writing
npx srcpack --no-emptyOutDir # Keep existing files in output directory
npx srcpack --no-emptyOutDir # Skip clearing the output directory
npx srcpack --no-upload # Bundle only, skip upload
npx srcpack init # Interactive config setup
npx srcpack login # Authenticate with Google Drive
Expand Down
13 changes: 11 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions docs/adr/005-on-demand-bundles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# ADR 005: On-demand bundles

**Status:** Accepted — 2026-09-13

## Context

Some bundles are not worth rebuilding on every `srcpack`: a Linear bundle costs a network round trip and a token, and a screenshot bundle needs a running dev server. Without a way to opt out, they either slow every run or fail it, and users drop them from the config — losing the "define once, rerun by name" workflow.

## Decision

A bundle may set `onDemand: true`. A full run (`srcpack`) skips it and prints `On demand: <names>`; naming it (`srcpack home`) builds it. `--dry-run` previews the same selection as the command it accompanies. If every bundle is on demand, a full run prints that line and exits 0.

The rule is the same for every source. Screenshot bundles are not on demand implicitly: a default that depends on source type is one more rule to learn.

Emptying does not change. A full run that empties `outDir` — by default only `.srcpack` — removes on-demand output inside it; the workflow is `srcpack`, then `srcpack home`. Where emptying is off, or an `outfile` sits outside `outDir`, that output survives and goes stale, exactly as a renamed bundle's does.

## Alternatives

- **`enabled: false`** — "disabled, unless named" contradicts itself.
- **Preserving on-demand output across full runs** — turns `emptyDirectory` into a growing list of exceptions, and custom outfiles make those exceptions paths rather than names. If it proves essential, an ownership manifest is the cleaner answer.

## Consequences

A full run no longer means "every configured bundle". Under the default `.srcpack`, output of an on-demand bundle does not survive a full run, which is visible (the file is gone) rather than stale.
59 changes: 59 additions & 0 deletions docs/adr/006-screenshot-bundles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# ADR 006: Screenshots as a bundle source

**Status:** Accepted — 2026-09-13

## Context

Reviewing a web app with a model means handing it the rendered page, not only the code. `page.screenshot()` is not enough on its own:

- **Tall pages become unreadable.** Vision models downscale every image to a fixed pixel budget, so a 1440×9000 page reaches them as a thumbnail.
- **The page isn't finished when `load` fires.** A full-page screenshot doesn't move the viewport, so lazy images and IntersectionObserver content stay blank.
- **Dev chrome pollutes the image.** Framework toolbars and cookie banners get reviewed as if they were the design.

And the workflow should be srcpack's: define once, rerun by name. A bundle stops meaning "sources → one text file" and becomes a named context packet — text, images, or both.

## Decision

**`screenshot` is a bundle source key**, the same shape as `linear`:

```ts
bundles: {
home: { screenshot: "http://localhost:5173/", onDemand: true },
pricing: {
include: "src/pages/pricing/**/*",
screenshot: { url: "http://localhost:5173/pricing", viewport: "mobile" },
},
}
```

A top-level bundle `url` would drag `viewport` and `hide` onto every bundle, where on a text bundle they would do nothing. `prompt`, `index` and `outfile` describe the text file, so on a screenshot-only bundle they are config errors — which is why `index` is no longer defaulted in the schema.

**Not generalizing to `sources: [{ provider }]`.** [ADR 003](./003-linear-issues-as-virtual-files.md) predicted that shape once a third source arrived. This is the third source, but `include` and `linear` produce entries in one text file while `screenshot` produces sibling binary files. A common provider abstraction would hide that difference rather than simplify it.

**Output is a flat numbered family, `<outDir>/<name>-NN.png`.** `00` is the whole page and `01…` are detail slices top to bottom, so lexical order is review order. Each image carries a logical index: an overview omitted for size leaves `01…` in place rather than renumbering a detail slice into `00`. There is no `outfile`, so a family always lives in `outDir` and removing stale images never reaches a directory srcpack doesn't own.

**A family is a claim on a directory plus a name prefix.** Its directory resolves fully — it is an ancestor of every PNG, unlike a text output's last component, which `rename` replaces (ADR 004). Families collide when their folded names match (`Web`/`web`); a text `outfile` collides with a family when it resolves into the family's directory under one of its numbered names, the same bundle's family included. Stale-image cleanup matches exactly, never folded, since folding could only widen a deletion.

**Configured and active writers are checked separately.** Configured bundles are checked against each other on every run, so a config error doesn't depend on what was asked for. The bundles a run writes — ad-hoc included — are checked against configured bundles of a different name, so `--screenshot` shadows a configured bundle named `screenshot` but cannot overwrite another bundle's file.

**Capture settles the page before it slices it.** It scrolls one viewport at a time, re-reading the page height so sections that load taller than their placeholders are followed, capped at 50 steps and 15 seconds with a warning. At each apparent bottom it waits for the network to go quiet and scrolls on if a response appended content, since growth measured but never visited is sliced with its lazy content blank. The wait uses its own in-flight request tracker: Playwright's `networkidle` load state resolves immediately once it has fired, which misses exactly the requests scrolling started. Slices are clipped captures of at most 2,200 device px: the fewest that cover the page with at least 160 px overlap, shrunk to share it evenly so a page just over the limit doesn't become two near-identical images; the overview is best-effort.

**Playwright is an optional peer**, resolved from the project first (`playwright`, then `@playwright/test`), then srcpack's own install. Under `npx`, srcpack runs from the npm cache where a bare import never sees the project's copy. The peer range is `*`: a project that never takes a screenshot shouldn't have its install judged against its Playwright version. Compatibility (1.41 up to 2) is checked at load instead, which also covers a copy found in the project, where a peer range constrains nothing. Without Playwright's Chromium, the system Chrome is used.

**Images stay local.** Drive uploads find files by name and update in place, so a page that shrank would leave old slices there with nothing to remove them. A mixed bundle's text file still uploads.

**`--dry-run` launches no browser.** Unlike Linear, there is no cheap way to answer "what would this produce" short of the capture itself, and a plain `fetch` would be a second code path that can pass where the capture then fails. The preview lists URL, viewport and destination; config and collision errors still surface.

## Alternatives

- **A top-level `url` on bundles** — rejected above.
- **`sources: [{ provider }]`** — hides the text-versus-files difference it claims to unify.
- **Stitching one tall image, or an image library for slicing** — clipped Playwright captures need no dependency, and a stitched image is exactly what downscaling ruins.
- **Several URLs per bundle** — numbering across pages hides which image is which page; `srcpack home pricing` already composes.
- **Uploading images** — needs Drive-side cleanup of shrunk pages first.

## Consequences

srcpack can now require a browser, but only for bundles that ask for one; nobody else downloads anything. Screenshot bundles need a running server, which is what `onDemand` ([ADR 005](./005-on-demand-bundles.md)) is for, and the unreachable-URL error in a full run says so.

Captured images are held in memory until every bundle resolves, so a failure anywhere leaves the previous run's files in place. As with text, each file is replaced atomically but a run is not. Authenticated pages, custom viewport sizes and dark mode are deferred.
2 changes: 2 additions & 0 deletions docs/adr/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ A record is never rewritten once accepted. When a decision is reversed, a new re
| [002](./002-minimum-node-version.md) | Node 22.18 as the minimum runtime | Accepted 2026-08-15 |
| [003](./003-linear-issues-as-virtual-files.md) | Linear issues as virtual files, under a `linear` bundle key | Accepted 2026-08-15 |
| [004](./004-path-boundaries.md) | What srcpack may read, delete and overwrite | Accepted 2026-08-15 |
| [005](./005-on-demand-bundles.md) | `onDemand` bundles: skipped by a full run, built when named | Accepted 2026-09-13 |
| [006](./006-screenshot-bundles.md) | Screenshots as a `screenshot` bundle source, written as numbered PNGs | Accepted 2026-09-13 |
Loading