Skip to content

feat: bundle git change sets with git: source tokens - #9

Merged
koistya merged 2 commits into
mainfrom
release/0.2.0
Aug 15, 2026
Merged

feat: bundle git change sets with git: source tokens#9
koistya merged 2 commits into
mainfrom
release/0.2.0

Conversation

@koistya

@koistya koistya commented Aug 15, 2026

Copy link
Copy Markdown
Member

Summary

Bundle patterns can now name git change sets, not just globs. Along the way: the runtime floor moves to Node 22.18, docs publishing moves off the gh-pages branch, and nine pre-existing bugs are fixed — four of them destructive.

git: source tokens

bundles: {
  review: ["git:staged", "!bun.lock"],
}
npx srcpack --staged       # one-off, no config file needed
npx srcpack --since main   # everything you changed on this branch
Source Files
git:staged Index vs HEAD
git:unstaged Worktree vs index (tracked only)
git:untracked New files not ignored by git
git:dirty All three
git:<rev> Changes vs <rev>, via merge base

Why a token, not a helper. The alternative — an exported $staged spread into the array — resolves eagerly, which runs git at config import time even for unrelated commands, rules out package.json config, and drops literal paths into an array that is later matched as globs, so a staged src/[id].tsx would silently match nothing. Tokens resolve lazily inside resolvePatterns(): no new config shape, no new exports, and ! exclusions compose for free. → ADR 001

Decisions a reviewer might otherwise question:

  • A source picks which files; content always comes from the worktree, so line citations match disk.
  • .gitignore doesn't apply — anything git reports is already tracked or filtered.
  • Deletions, binaries, submodules and symlinks are skipped.
  • Ad-hoc bundles never upload: upload.exclude can't name a bundle that exists for one run.

Node 22.18 floor

cosmiconfig 10 drops its bundled typescript for Node's built-in type stripping. srcpack.config.ts is the primary config format, so runtime TS loading isn't optional — the floor follows cosmiconfig's own range rather than a looser invented one. A >=20 floor would install cleanly, then fail on the first .ts config with a parse error instead of an engine warning.

Published CLI: 10.8 MB → 2.4 MB. Node 18 and 20 are both EOL. → ADR 002

Two consequences of type stripping:

  • Config files must use erasable syntax — enum and namespace are out.
  • Node reads a .ts file's module format from the nearest package.json, so a .ts config's import line is a syntax error in a CommonJS project — which cosmiconfig 9's bundled compiler used to hide. srcpack.config.mts joins searchPlaces, and init writes it when the project isn't "type": "module".

The suite runs on Bun, which loads either extension regardless of package type and so can't see that second one. CI now installs the packed tarball into a CommonJS project and runs the CLI under Node.

Fixes

Destructive, each reproduced before fixing:

Bug Symptom
outDir: "." Deleted the entire project — sources, config, node_modules — then printed Bundled: 1 bundle and exited 0. Now refused outright.
Self-bundling Each rerun bundled the previous run's output, nesting one level deeper (239 → 556 → 874 bytes over three runs). srcpack never bundles what srcpack writes.
srcpack web Emptied all of outDir, destroying api.txt — which that run could not rebuild. Emptying now happens only on a full run.
Symlinks followed A tracked notes.txt -> ~/.ssh/id_rsa was read into the bundle and uploaded under an innocuous name. Now lstat, so symlinks are skipped.
Security and correctness (5 more)
Bug Symptom
Drive query injection A ' in a bundle filename made findFile resolve to a different file, which the upload then overwrote. Name and folderId are escaped now.
Token permissions ~/.config/srcpack/credentials.json was written 0644 — a refresh token readable by any account on the machine. Now 0600, with a chmod so files from older versions are repaired.
init codegen Hyphenated names emitted invalid TypeScript (my-app: "…"), and backslashes were silently dropped (src\**\*src***). All values go through JSON.stringify.
Upload failures Caught, printed, then exited 0 — CI saw green after a failed upload.
--since init Ran the init wizard instead of diffing against the init branch. Subcommands now match in first position only.

Every fix has a regression test, each mutation-tested to confirm the test fails without it.

Docs publishing

Deploys via actions/deploy-pages on release: published, replacing the gh-pages branch and devDependency. docs:build now runs in CI, so broken links fail at PR time. ADRs became a linked section of the site instead of orphan pages.

Three more bugs fixed here: a live 404 on /srcpack/favicon.ico (referenced but never present), a shallow checkout that collapsed every page's "Last updated" to the release commit, and prereleases republishing the site.

Important

Merge before cutting 0.2.0release and workflow_dispatch only run workflow files from the default branch, so docs.yml must be on main or the deploy silently no-ops.

The Pages source is still legacy/gh-pages and must be switched by hand. The site is unpublished until the first Actions deploy lands (~1–2 min):

gh api -X PUT repos/kriasoft/srcpack/pages -f build_type=workflow
gh workflow run docs.yml
# verify kriasoft.com/srcpack/, then: git push origin --delete gh-pages

This can't cost the custom domain: the repo's Pages cname is null and /srcpack/CNAME 404s — kriasoft.com is inherited from the org site, not set by this repo.

A pattern may now name a set of changed files instead of a glob:
`git:staged`, `git:unstaged`, `git:untracked`, `git:dirty`, or `git:<rev>`.
CLI flags `--staged`, `--dirty` and `--since <rev>` build the same bundle
ad hoc, with no config file at all.

Globs cannot express "what I'm currently changing", which is the most common
reason to hand code to an LLM. Resolving the set lazily inside the pattern
array keeps the config plain data — it still works in package.json, composes
with `!` exclusions, and concrete paths never get re-matched as globs (a
staged `src/[id].tsx` would silently match nothing). See ADR 001.

Also raises the Node floor to ^22.18 so cosmiconfig 10 can load
`srcpack.config.ts` via Node's built-in type stripping instead of a bundled
TypeScript parser, cutting the published CLI from 10.8 MB to 2.4 MB. See
ADR 002.

Fixes found while reviewing the above:

- `outDir: "."` deleted the entire project, then reported success
- reruns bundled their own output, nesting it deeper each time
- `srcpack web` emptied outDir, destroying bundles it could not rebuild
- symlinks were followed, so a tracked link could pull in a file from
  outside the project and upload it
- a quote in a bundle filename could make the Drive query resolve to an
  unrelated file, which the upload then overwrote
- OAuth refresh tokens were written world-readable (0644)
- `srcpack init` generated invalid TypeScript for hyphenated bundle names,
  and dropped backslashes from patterns
- failed uploads exited 0
- `--since init` ran the init wizard instead of diffing against `init`

Docs now deploy from GitHub Actions rather than the gh-pages branch.
Node derives a `.ts` file's module format from the nearest package.json
`type`, so in a CommonJS project — the `npm init` default — the generated
config's `import { defineConfig }` line is a syntax error. cosmiconfig 9
compiled the file with its bundled TypeScript and hid this; cosmiconfig 10
type-strips it, so 0.2.0 would have shipped broken for those projects.

`srcpack.config.mts` joins searchPlaces and `init` writes it whenever the
project is not `"type": "module"`. `.mts` is unconditionally ESM and loads
either way; `.ts` stays the default for ESM projects since that is the name
the docs use.

The suite runs on Bun, which loads either extension regardless of package
type and so cannot see this failure. CI now installs the packed tarball into
a CommonJS project and runs the CLI under Node.
@koistya
koistya merged commit 63356a5 into main Aug 15, 2026
1 check passed
@koistya
koistya deleted the release/0.2.0 branch August 15, 2026 11:51
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