diff --git a/GUIDE.md b/GUIDE.md index 8ad470b..5a898e7 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -495,6 +495,8 @@ const treeOid = await cas.createTree({ manifest }); ## 7. The CLI +git-cas CLI demo + `git-cas` installs as a Git subcommand. After installation, `git cas` is available in any Git repository. @@ -963,6 +965,8 @@ console.log(manifest.chunks.length); // Full chunk list, regardless of structur ## 13. Vault +git-cas vault demo + When you call `createTree({ manifest })`, the resulting tree is a loose Git object. If nothing references it -- no commit, no tag, no ref -- `git gc` will garbage-collect it. This can silently lose stored data. diff --git a/README.md b/README.md index 08f2f8a..cf1ee5e 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ We use the object database. **Use it for:** binary assets, build artifacts, model weights, data packs, secret bundles, weird experiments, etc. +git-cas demo + ## What's new in v2.0.0 **Compression** — `compression: { algorithm: 'gzip' }` on `store()`. Compression runs before encryption. Decompression on `restore()` is automatic. @@ -122,10 +124,11 @@ git cas vault info my-image git cas vault remove my-image git cas vault history -# Encrypted vault round-trip -git cas vault init --vault-passphrase "secret" -git cas store ./secret.bin --slug vault-entry --tree --vault-passphrase "secret" -git cas restore --slug vault-entry --out ./decrypted.bin --vault-passphrase "secret" +# Encrypted vault round-trip (passphrase via env var or --vault-passphrase flag) +export GIT_CAS_PASSPHRASE="secret" +git cas vault init +git cas store ./secret.bin --slug vault-entry --tree +git cas restore --slug vault-entry --out ./decrypted.bin ``` ## Why not Git LFS? diff --git a/ROADMAP.md b/ROADMAP.md index 8ee815b..e384587 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1673,3 +1673,30 @@ git-cas occupies a specific niche: **Git-native encrypted content-addressed stor What it is: the only tool that lets you `git cas store ./model.bin --slug v3-weights --tree --vault-passphrase "secret"`, commit the tree OID, push to any Git remote, and restore it on any machine with `git cas restore --slug v3-weights --out ./model.bin --vault-passphrase "secret"` — no server, no external storage, no second system. Everything is Git objects, Git refs, Git transport. If that's what you want, nothing else does it. If it's not, the right tool probably isn't git-cas. + +--- + +## Backlog (unscheduled) + +Ideas for future milestones. Not committed, not prioritized — just captured. + +### Named Vaults +Multiple vaults instead of one. Refs move from `refs/cas/vault` to `refs/cas/vaults/`. Default vault is `default`. CLI gets `--vault ` flag. + +### Export +- **Export vault to archive** — `git cas vault export --format tar.gz` dumps all entries to a tarball/zip. +- **Export individual entry** — `git cas export --slug photos/vacation --format tar.gz` restores and archives a single entry. +- **Bulk export** — restore multiple slugs into a single archive. + +### Vault Management +- **Move into vault** — `git cas vault add --slug --oid ` to adopt an existing CAS tree into the vault (the API `addToVault()` already supports this; just needs a CLI command). +- **Purge from CAS** — remove an entry from the vault and run `git gc` to reclaim storage. Tricky because git doesn't delete individual objects — you remove refs and let GC handle it. + +### Publish / Mount +- **Publish to working tree** — `git cas publish --slug assets/hero --to docs/hero.gif` reconstitutes a vault entry into the repo's working tree so it's servable by GitHub (markdown images, Pages, etc.). +- **Publish to branch** — `git cas publish --branch gh-assets` materializes all vault entries onto a dedicated branch. Keeps the main branch clean while making assets accessible via GitHub raw URLs. +- **Auto-publish hook** — pre-commit or CI step that keeps published assets in sync with vault state. + +### Repo Intelligence +- **Duplicate detection on store** — warn if a file being stored already exists as a tracked git blob (same content hash). "This file is already tracked by git — are you sure you want to store it in CAS too?" +- **Repo scan / dedup advisor** — `git cas scan` walks the git object database and recommends files that could benefit from CAS (large blobs, binary files, duplicated content across branches). Reports dedup opportunities and potential storage savings. diff --git a/bin/git-cas.js b/bin/git-cas.js index 7dff41c..28b3931 100755 --- a/bin/git-cas.js +++ b/bin/git-cas.js @@ -45,20 +45,28 @@ async function deriveVaultKey(cas, metadata, passphrase) { } /** - * Resolve encryption key from --key-file or --vault-passphrase. + * Resolve passphrase from --vault-passphrase flag or GIT_CAS_PASSPHRASE env var. + */ +function resolvePassphrase(opts) { + return opts.vaultPassphrase ?? process.env.GIT_CAS_PASSPHRASE; +} + +/** + * Resolve encryption key from --key-file or --vault-passphrase / GIT_CAS_PASSPHRASE. */ async function resolveEncryptionKey(cas, opts) { if (opts.keyFile) { return readKeyFile(opts.keyFile); } - if (!opts.vaultPassphrase) { + const passphrase = resolvePassphrase(opts); + if (!passphrase) { return undefined; } const metadata = await cas.getVaultMetadata(); if (metadata?.encryption) { - return deriveVaultKey(cas, metadata, opts.vaultPassphrase); + return deriveVaultKey(cas, metadata, passphrase); } - process.stderr.write('warning: --vault-passphrase ignored (vault is not encrypted)\n'); + process.stderr.write('warning: passphrase ignored (vault is not encrypted)\n'); return undefined; } @@ -100,7 +108,7 @@ program .option('--key-file ', 'Path to 32-byte raw encryption key file') .option('--tree', 'Also create a Git tree and print its OID') .option('--force', 'Overwrite existing vault entry') - .option('--vault-passphrase ', 'Vault-level passphrase for encryption') + .option('--vault-passphrase ', 'Vault-level passphrase for encryption (prefer GIT_CAS_PASSPHRASE env var)') .option('--cwd ', 'Git working directory', '.') .action(async (file, opts) => { try { @@ -157,7 +165,7 @@ program .option('--slug ', 'Resolve tree OID from vault slug') .option('--oid ', 'Direct tree OID') .option('--key-file ', 'Path to 32-byte raw encryption key file') - .option('--vault-passphrase ', 'Vault-level passphrase for decryption') + .option('--vault-passphrase ', 'Vault-level passphrase for decryption (prefer GIT_CAS_PASSPHRASE env var)') .option('--cwd ', 'Git working directory', '.') .action(async (opts) => { try { @@ -194,15 +202,16 @@ const vault = program vault .command('init') .description('Initialize the vault') - .option('--vault-passphrase ', 'Passphrase for vault-level encryption') + .option('--vault-passphrase ', 'Passphrase for vault-level encryption (prefer GIT_CAS_PASSPHRASE env var)') .option('--algorithm ', 'KDF algorithm (pbkdf2 or scrypt)', 'pbkdf2') .option('--cwd ', 'Git working directory', '.') .action(async (opts) => { try { const cas = createCas(opts.cwd); const initOpts = {}; - if (opts.vaultPassphrase) { - initOpts.passphrase = opts.vaultPassphrase; + const passphrase = resolvePassphrase(opts); + if (passphrase) { + initOpts.passphrase = passphrase; initOpts.kdfOptions = { algorithm: opts.algorithm }; } const { commitOid } = await cas.initVault(initOpts); diff --git a/docs/cli.gif b/docs/cli.gif new file mode 100644 index 0000000..767d095 Binary files /dev/null and b/docs/cli.gif differ diff --git a/docs/demo.gif b/docs/demo.gif new file mode 100644 index 0000000..7de5049 Binary files /dev/null and b/docs/demo.gif differ diff --git a/docs/vault.gif b/docs/vault.gif new file mode 100644 index 0000000..302e659 Binary files /dev/null and b/docs/vault.gif differ