feat(core): add lockfile-backed caplet catalog installs#167
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds catalog docs and entries, lockfile path and validation support, lockfile-aware install and update flows, remote-global routing, and tests covering lockfile, CLI, and remote behavior. ChangesPrebuilt Caplets Catalog and Lockfile Rollout
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Preview DeployedLanding: https://pr-167.preview.caplets.dev Built from commit bb4d8d0 |
|
| Filename | Overview |
|---|---|
| packages/core/src/cli/install.ts | Large new feature: adds restoreCapletsFromLockfile, updateCapletsFromLockfile, risk inference, and per-entry lockfile writes. Previous P1 bugs (temp clone cleanup, partial-install lockfile gaps, dead ternaries) are addressed. New P2 findings: full git clone on update path; cliTools mutating default; destinationDisplay fragility. |
| packages/core/src/cli/lockfile.ts | New lockfile module: atomic write via PID/timestamp temp name, ENOENT vs CONFIG_INVALID correctly differentiated, path-traversal checks for destinations, credential-stripped source validation. Solid implementation. |
| packages/core/src/cli.ts | Adds restore (no-arg install) and update commands with JSON output, per-target lockfile path resolution, and isInstallSourceArgument disambiguation. parseCatalogLifecycleTarget correctly allows --remote --global combination. |
| packages/core/src/remote-control/dispatch.ts | Remote install now targets global root (matching PR intent), adds update command dispatching. globalCapletsRoot and globalLockfilePath are correctly threaded from context. |
| packages/core/test/cli.test.ts | Extensive new test coverage for lockfile writes on install, partial-install recovery, JSON output, restore/noop behavior, and update flows including remote global scenarios. |
| packages/core/test/caplets-lockfile.test.ts | New unit tests for lockfile read/write atomicity, malformed input rejection, ENOENT vs invalid-JSON error differentiation, and destination symlink/path-traversal validation. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["caplets install [repo] [ids...]"] --> B{repo arg present?}
B -- "yes → isInstallSourceArgument?" --> C{Is source arg?}
C -- yes --> D[installCaplets\nclone/copy source]
C -- no --> E[restoreCapletsFromLockfile\nids = prepend repo to ids]
B -- no --> E
D --> F[installOneCaplet × N\nper-entry]
F --> G[hashInstalledArtifact]
G --> H[updateLockfileAfterInstall\nwrite lockfile per-entry]
I["caplets update [ids...]"] --> J[updateCapletsFromLockfile]
J --> K{destination exists?}
K -- yes --> L{currentHash == installedHash?}
L -- no + !force --> M[throw CONFIG_EXISTS]
L -- yes or force --> N[resolveLockedSource\nfull git clone]
K -- no --> N
N --> O[sourceHash == installedHash\n+ !force?]
O -- yes noop --> P[update lockfile metadata\nif source/risk changed]
O -- no --> Q{riskIncrease + !force?}
Q -- yes --> R[throw REQUEST_INVALID]
Q -- no --> S[installOneCaplet force=true]
S --> T[writeCapletsLockfile\nper-entry]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["caplets install [repo] [ids...]"] --> B{repo arg present?}
B -- "yes → isInstallSourceArgument?" --> C{Is source arg?}
C -- yes --> D[installCaplets\nclone/copy source]
C -- no --> E[restoreCapletsFromLockfile\nids = prepend repo to ids]
B -- no --> E
D --> F[installOneCaplet × N\nper-entry]
F --> G[hashInstalledArtifact]
G --> H[updateLockfileAfterInstall\nwrite lockfile per-entry]
I["caplets update [ids...]"] --> J[updateCapletsFromLockfile]
J --> K{destination exists?}
K -- yes --> L{currentHash == installedHash?}
L -- no + !force --> M[throw CONFIG_EXISTS]
L -- yes or force --> N[resolveLockedSource\nfull git clone]
K -- no --> N
N --> O[sourceHash == installedHash\n+ !force?]
O -- yes noop --> P[update lockfile metadata\nif source/risk changed]
O -- no --> Q{riskIncrease + !force?}
Q -- yes --> R[throw REQUEST_INVALID]
Q -- no --> S[installOneCaplet force=true]
S --> T[writeCapletsLockfile\nper-entry]
Reviews (12): Last reviewed commit: "fix(catalog): remove default shadowing p..." | Re-trigger Greptile
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
packages/core/src/remote-control/dispatch.ts (1)
134-170: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: hoist
destinationRoot/lockfilePathto avoid duplicate resolution.The
updatebranch recomputes the samecontext.globalCapletsRoot ?? resolveCapletsRoot(context.configPath)andcontext.globalLockfilePath ?? defaultCapletsLockfilePath()expressions already derived in theinstallbranch (Lines 135-136). Computing them once near the top ofdispatch(or in a small helper) keeps the global-routing contract in a single place and avoids drift if the fallback logic changes.🤖 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 `@packages/core/src/remote-control/dispatch.ts` around lines 134 - 170, The `dispatch` command handling repeats the same caplets destination and lockfile fallback logic in both the `install` and `update` branches, which can drift over time. Hoist the `destinationRoot` and `lockfilePath` resolution into shared locals near the top of `dispatch` (or into a small helper) and reuse them in both `installCaplets`/`restoreCapletsFromLockfile` and `updateCapletsFromLockfile` so the global routing fallback is defined once.
🤖 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 `@packages/core/src/cli.ts`:
- Around line 2907-2919: The current Commander.js argument order in the install
command makes a single positional value get parsed as repo, so specific lockfile
restore IDs are never reached. Update the command definition and the action
handler around the install flow so restore-by-ID is unambiguous: either move
restore IDs off the positional `[repo]`/`[caplets...]` parsing (for example via
a dedicated flag or subcommand) or reorder/split the arguments so `repo` no
longer captures single-ID restore cases. Make sure the logic that decides
restore mode in the install action still distinguishes repo installs from
lockfile restores using the relevant symbols in `cli.ts` (the command builder
and its `.action` handler).
In `@packages/core/src/cli/install.ts`:
- Around line 465-488: The git restore path resolution in
resolveLockedSourcePath leaks the temporary repo created by mkdtempSync because
it only returns the checked-out path and never exposes cleanup. Change the flow
so resolveLockedSourcePath returns both the resolved path and a cleanup callback
for the temp root, then update the restore/update call sites to invoke that
cleanup in a finally block. Use the existing resolveLockedSourcePath and any
restore/update logic in install.ts to locate the affected code.
- Around line 273-284: The update path in updateCapletsFromLockfile is reusing
resolveLockedSourcePath(entry.source), which reads source.resolvedRevision and
prevents git-backed entries from picking up newer upstream changes. Adjust the
git update flow so restore/install still uses the pinned revision, but update
resolves from the tracked ref in the lock entry, then writes back the newly
resolved revision to the lockfile. Use the updateCapletsFromLockfile and
resolveLockedSourcePath symbols to locate the change and keep the risk/hash
checks operating on the newly fetched source.
- Around line 731-744: localGitInfo currently hardcodes dirty: false for every
local lock entry, which can mark a worktree as clean even when uncommitted
changes were present. Update localGitInfo to determine the dirty state from the
repository status (for example via git status --porcelain) before returning the
Partial<Extract<CapletsLockSource, { type: "local" }>>, and only include dirty
when you can confidently derive it; otherwise omit the field. Keep the existing
gitRepository and gitRevision handling intact.
- Around line 204-214: The restore flow in install.ts writes the lockfile from
lockfile.entries inside the hash-change path, so each changed entry overwrites
prior updates from the same restore run. Update the logic in the restore loop to
accumulate changes into a shared snapshot/map keyed by entry.id, then call
writeCapletsLockfile once after processing all entries, using the merged entries
with installedHash and updatedAt applied for every restored item.
In `@packages/core/src/cli/lockfile.ts`:
- Around line 80-85: The lockfile write path currently uses a shared temporary
filename, which can collide across concurrent writers. Update the temp-file
creation in the lockfile write flow around the temporary path in the lockfile
helper to generate a unique per-write temp name (for example by adding a random
or process-specific suffix) before calling mkdirSync, writeFileSync, and
renameSync. Keep the final rename to the target lockfile unchanged, but ensure
each invocation of the lockfile write routine uses its own temp file so
concurrent installs/updates do not interfere with one another.
- Around line 174-179: The git lockfile parser in the lockfile schema currently
accepts source.path values like “..”, which can later escape the cloned
repository when joined with the clone root. Update the validation in the
lockfile parsing/normalization path for git entries, using the existing
source.path handling in the lockfile.ts logic, to reject unsafe relative paths
(for example, any path containing traversal segments or otherwise not staying
within the repository root). Keep the check close to where
requireString(value.path, ...) is used so tampered lockfiles are rejected early.
---
Nitpick comments:
In `@packages/core/src/remote-control/dispatch.ts`:
- Around line 134-170: The `dispatch` command handling repeats the same caplets
destination and lockfile fallback logic in both the `install` and `update`
branches, which can drift over time. Hoist the `destinationRoot` and
`lockfilePath` resolution into shared locals near the top of `dispatch` (or into
a small helper) and reuse them in both
`installCaplets`/`restoreCapletsFromLockfile` and `updateCapletsFromLockfile` so
the global routing fallback is defined once.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 6f6cb608-2143-4376-a742-a71a93bbc7e2
📒 Files selected for processing (32)
.changeset/prebuilt-caplets-lockfile-update.mdCONCEPTS.mdREADME.mdapps/docs/src/content/docs/install.mdxcaplets/ast-grep/CAPLET.mdcaplets/browser-use/CAPLET.mdcaplets/computer-use/CAPLET.mdcaplets/gmail/CAPLET.mdcaplets/google-drive/CAPLET.mdcaplets/google-tasks/CAPLET.mdcaplets/lsp/CAPLET.mdcaplets/posthog/CAPLET.mdcaplets/repo-cli/CAPLET.mdcaplets/sentry/CAPLET.mdcaplets/stealth-browser-use/CAPLET.mddocs/brainstorms/2026-06-26-prebuilt-caplets-catalog-requirements.mddocs/plans/2026-06-26-001-feat-prebuilt-caplets-catalog-and-lockfile-update-plan.mdpackages/core/src/caplet-files-bundle.tspackages/core/src/cli.tspackages/core/src/cli/commands.tspackages/core/src/cli/install.tspackages/core/src/cli/lockfile.tspackages/core/src/config.tspackages/core/src/config/paths.tspackages/core/src/remote-control/dispatch.tspackages/core/src/remote-control/types.tspackages/core/src/serve/http.tspackages/core/test/caplets-lockfile.test.tspackages/core/test/cli.test.tspackages/core/test/config-paths.test.tspackages/core/test/remote-control-dispatch.test.tsskills/writing-caplets/SKILL.md
💤 Files with no reviewable changes (1)
- packages/core/src/caplet-files-bundle.ts
a703feb to
bc7cdec
Compare
bc7cdec to
037d9fa
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@packages/core/src/cli/install.ts`:
- Around line 302-313: The install flow in install.ts is leaving stale lockfile
provenance when an install is a no-op or when reinstalling from a local source.
Update the logic around the existing/noop branch and the update branch to derive
a refreshed source from the current lock source, preserving git resolvedRevision
and local git metadata via localGitInfo(lockedSource.repoRoot) as appropriate.
In Install/lockfile handling, compare the refreshed source against the stored
one and write the lockfile whenever provenance changes, even if the artifact
hash is unchanged and the install status is noop.
In `@packages/core/test/cli.test.ts`:
- Around line 4323-4328: The git fixture environment is not hermetic because
gitFixtureEnv() only clears repository-specific variables and still allows
global/system Git config to influence tests. Update gitFixtureEnv() so the env
passed to execFileSync/execSync also disables global and system config by
setting the appropriate Git environment variables, keeping the fixture isolated
from machine-level settings.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9ab55bff-c8ff-47a0-b629-78ef38fe843b
📒 Files selected for processing (6)
packages/core/src/cli.tspackages/core/src/cli/install.tspackages/core/src/cli/lockfile.tspackages/core/src/remote-control/dispatch.tspackages/core/test/caplets-lockfile.test.tspackages/core/test/cli.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/core/test/caplets-lockfile.test.ts
- packages/core/src/cli.ts
- packages/core/src/remote-control/dispatch.ts
There was a problem hiding this comment.
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 `@packages/core/src/cli/install.ts`:
- Around line 302-312: Persist the refreshed risk metadata during no-op installs
in install.ts: in the existing branch around existing/sourceHash/!options.force,
the lockfile update only happens when sameLockSource(entry.source, nextSource)
is false, so nextRisk computed earlier is dropped. Update the logic in this path
to also write the entry’s risk from nextRisk (and keep updatedAt/source handling
as needed) whenever the lockfile entry is being refreshed, even if the artifact
hash and source are unchanged, using the existing nextEntries set and
writeCapletsLockfile flow.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 9a9a2aa7-5d23-4b30-a5cc-ec89df3a1e4c
📒 Files selected for processing (5)
packages/core/src/cli.tspackages/core/src/cli/install.tspackages/core/src/cli/lockfile.tspackages/core/test/caplets-lockfile.test.tspackages/core/test/cli.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/core/test/caplets-lockfile.test.ts
- packages/core/test/cli.test.ts
- packages/core/src/cli/lockfile.ts
- packages/core/src/cli.ts
There was a problem hiding this comment.
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 `@packages/core/src/cli/install.ts`:
- Around line 303-304: The no-op refresh path in install.ts is blocked too early
by the risk-increase guard, so unchanged artifacts cannot persist refreshed
metadata. In the install/update flow around sourceHash and the
sourceChanged/riskChanged checks, move the guard that rejects increased risk to
after the branch that handles sourceHash === entry.installedHash, and let that
branch update the installed entry’s source and risk first. Keep the existing
block only for updates that actually change the artifact, using the same
installed artifact metadata path and related helpers in that section.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: bc0ec62f-ff15-4e66-8d41-f7dde645a529
📒 Files selected for processing (2)
packages/core/src/cli/install.tspackages/core/test/cli.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/core/test/cli.test.ts
Summary
Caplet installs can now be treated as reproducible lifecycle operations instead of one-off file copies. Installs record source provenance, git revision, destination, artifact hash, and derived risk in a lockfile; no-argument
caplets installrestores that lockfile, andcaplets updaterefreshes tracked Caplets globally, per-project, or on a remote machine's global root.The prebuilt catalog now has initial browser, desktop, observability, Google Workspace, and local-project Caplets under
caplets/, with project-bound entries declaring their required project binding. A newwriting-capletsskill captures the authoring conventions for future Caplets without adding author-maintained catalog metadata.Design Notes
./.caplets.lock.jsonfor project installs and the Caplets state directory for global installs.catalogproperty.--remoteinstall/update targets the remote machine's global Caplets root and lockfile.--forceis passed.Validation
pnpm verifypassed in the pre-push hook.Summary by CodeRabbit
caplets install(writes lockfiles),caplets installno-argument restore, and newcaplets updatewith project/global/remote targeting and--json.--force, and applies atomic, integrity-focused lockfile handling.