Version 0.3.0: resolver/merge/cache refactor, signing design notes
- Split the monolith into package/provider/fetch/merge/cache modules with the Resolver deciding install/adopt/update/merge/conflict outcomes - Three-way merges via git merge-file/diff3 against a content-addressed merge-base cache; conflicts go to a .picopackage-merge sibling - Tests for package, provider, resolver, merge, and cache - notes.md: update UX and signing design — diff-by-default, SSH signature identity pinning (TOFU), key changes as a hard stop, exit-code contract - Add CLAUDE.md; remove the pre-refactor exe/pppkg monolith and scratch files Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EiyJC73Mz8xZyCTvCEY8qn
This commit is contained in:
co-authored by
Claude Fable 5
parent
e0cd0f0d7a
commit
6dd57e84f1
@@ -1,6 +1,11 @@
|
||||
# Picopackage
|
||||
|
||||
A command line tool for installing and managing [Picopackages](https://picopackage.org).
|
||||
A command line tool for installing and updating [picopackages](https://picopackage.org):
|
||||
single files that carry their own provenance, so you can install one into your
|
||||
project, edit it, and still pull upstream fixes later.
|
||||
|
||||
For code that's too small to be a gem but copied often enough that pasting it
|
||||
between projects hurts.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -10,18 +15,121 @@ gem install picopackage
|
||||
|
||||
## Usage
|
||||
|
||||
`picopackage install <url|filepath>`
|
||||
```bash
|
||||
ppkg install https://gist.github.com/you/abc123 # fetch into .
|
||||
ppkg install https://example.com/retry.rb lib/ # fetch into lib/
|
||||
ppkg update lib/retry.rb # re-fetch from its recorded url
|
||||
ppkg package lib/mine.rb --url https://… # turn a local file into a picopackage
|
||||
ppkg verify lib/retry.rb # check it against its checksum
|
||||
ppkg scan . # list picopackages in a tree
|
||||
ppkg inspect lib/retry.rb # print its metadata
|
||||
```
|
||||
|
||||
## What a picopackage looks like
|
||||
|
||||
An ordinary source file with a commented YAML block at the end:
|
||||
|
||||
```ruby
|
||||
def retry_with_backoff(attempts: 3)
|
||||
# …
|
||||
end
|
||||
|
||||
# @PICOPACKAGE_START
|
||||
# ---
|
||||
# url: https://gist.github.com/you/abc123
|
||||
# filename: retry.rb
|
||||
# payload_timestamp: '2026-08-12T14:06:38Z'
|
||||
# payload_checksum: sha256:7b856b1b…
|
||||
# base_checksum: sha256:7b856b1b…
|
||||
# @PICOPACKAGE_END
|
||||
```
|
||||
|
||||
Everything above the block is the **payload**. The block is regenerated on every
|
||||
write. Metadata keys the tool doesn't recognise are preserved as-is, so optional
|
||||
fields survive a tool that predates them.
|
||||
|
||||
## Upstream doesn't need a block
|
||||
|
||||
A plain file with no metadata at all is a valid picopackage source. You can
|
||||
install someone's gist, blog snippet or raw file without them adopting anything
|
||||
— everything the block would have said is derived from the fetch.
|
||||
|
||||
When upstream *does* carry a block, it's opting in to extra guarantees, and only
|
||||
half of it is upstream's to assert:
|
||||
|
||||
| | Fields | Who decides |
|
||||
| --- | --- | --- |
|
||||
| **Claims** | `url`, `filename`, `payload_version`, `licence` | upstream — adopted as given |
|
||||
| **Records** | `payload_checksum`, `base_checksum` | your machine — always recomputed |
|
||||
|
||||
A claim is something only the author knows: the canonical URL that outlives the
|
||||
mirror you fetched from, the version, the licence. A record describes one copy on
|
||||
one disk, so upstream can't speak for yours. In particular an upstream
|
||||
`payload_checksum` is never trusted — a checksum stored beside the content it
|
||||
hashes proves nothing, and an author who edits without re-running the tool ships
|
||||
a stale one. Adopting it would install a file that failed its own verification
|
||||
and reported as locally modified before you touched it.
|
||||
|
||||
## Updating a file you've edited
|
||||
|
||||
The point of a picopackage is that it's *your* file — so editing it has to be
|
||||
allowed, and updates have to cope with that. `ppkg update` does a three-way
|
||||
merge against the version you originally installed:
|
||||
|
||||
- **Unchanged locally** → fast-forwarded to upstream.
|
||||
- **Edited locally, upstream unchanged** → nothing happens. No merge is
|
||||
attempted; upstream is still the version you branched from.
|
||||
- **Edited locally, no overlap with upstream's changes** → merged, keeping both.
|
||||
- **Edited locally, overlapping changes** → refused. Your file is left exactly
|
||||
as it is and a copy with conflict markers is written to
|
||||
`<file>.picopackage-merge` for you to work from.
|
||||
- `--force` discards local changes and takes upstream wholesale.
|
||||
|
||||
Two rules decide every case:
|
||||
|
||||
1. **The checksum decides, never the timestamp.** An mtime says nothing about
|
||||
content — a file re-uploaded unchanged gets a fresh one.
|
||||
2. **Merges run on the payload alone.** The metadata block is regenerated
|
||||
afterwards, so it never conflicts.
|
||||
|
||||
The merge needs the common ancestor, which is kept in a content-addressed cache
|
||||
under `$XDG_CACHE_HOME/picopackage` (override with `PICOPACKAGE_CACHE`). A file
|
||||
installed on another machine, or one whose cache has been cleared, has no
|
||||
ancestor available — `ppkg` says so and refuses rather than guessing.
|
||||
|
||||
`git merge-file` is used when available, `diff3` otherwise.
|
||||
|
||||
## Sources
|
||||
|
||||
| Source | Handling |
|
||||
| --- | --- |
|
||||
| GitHub Gist | Resolved through the Gist API |
|
||||
| Opengist | Resolved via its `.json` endpoint |
|
||||
| Any URL | Fetched directly |
|
||||
| Local path | Read from disk |
|
||||
|
||||
Responses are capped at 1 MB and time out after 10s. Redirects are followed up
|
||||
to 5 times and only to `http`/`https`.
|
||||
|
||||
## What it deliberately doesn't do
|
||||
|
||||
- **No dependencies.** Each file stands alone. There is no solver, no version
|
||||
ranges, no transitive graph.
|
||||
- **No registry.** The file's `url` is its identity; no central index exists.
|
||||
- **No author verification.** A checksum stored beside the content it hashes
|
||||
proves nothing about who wrote it — its job is detecting *local* modification.
|
||||
Signing (SSH signatures, TOFU-pinned — see notes.md) is designed, not
|
||||
implemented. Until then, the security model is that a picopackage is one
|
||||
file and you can read it.
|
||||
|
||||
## Development
|
||||
|
||||
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
||||
`bin/setup` installs dependencies, `rake test` runs the tests, `rake` runs tests
|
||||
and [Standard](https://github.com/standardrb/standard). `bin/console` gives you a
|
||||
prompt.
|
||||
|
||||
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
||||
To release: update `lib/picopackage/version.rb`, then `bundle exec rake release`.
|
||||
|
||||
## Contributing
|
||||
## Licence
|
||||
|
||||
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/picop.
|
||||
|
||||
## License
|
||||
|
||||
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
||||
MIT.
|
||||
|
||||
Reference in New Issue
Block a user