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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ jobs:
- name: Typecheck
run: pnpm typecheck

- name: Build
run: pnpm build

- name: Test
run: pnpm test:coverage

Expand All @@ -47,9 +50,6 @@ jobs:
with:
command: audit

- name: Build
run: pnpm build

- name: Size limit
run: pnpm size

Expand Down
58 changes: 45 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,17 @@ Deterministic local caching of external documentation for agents and developers

Provides agents and developers with local access to external documentation without committing it to the repository.

Documentation is cached in a gitignored location, exposed to agent and tool targets via links or copies, and updated through sync commands or postinstall hooks.
Documentation is cached in a gitignored location. When enabled, docs-cache exposes sources to OpenCode as local references. Optional links or copies remain available for other tools.

## Features

- **Local only**: Cache lives in the directory `.docs` (or a custom location) and can be gitignored.
- **Deterministic**: `docs-lock.json` pins commits and file metadata.
- **Fast**: Local cache avoids network roundtrips after sync.
- **Flexible**: Cache full repos or just the subdirectories you need.
- **OpenCode-aware**: Detected OpenCode configs can expose every source as an `@` reference.

> **Note**: Sources are downloaded to a local cache. If you provide a `targetDir`, `docs-cache` creates a symlink or copy from the cache to that target directory.
> **Note**: Sources always materialize in the local cache. `targetDir` is optional and only creates a symlink or copy for tools that need a separate physical path.

## Usage

Expand Down Expand Up @@ -51,14 +52,18 @@ npx docs-cache clean

> for more options: `npx docs-cache --help`

## Requirements

- Node.js 24 or later

## Recommended Workflow

Use this flow to keep behavior predictable (similar to package manager manifest + lock workflows):

1. Keep source intent in config (`ref: "main"`, `ref: "v1"`, or a commit SHA).
2. Run `npx docs-cache update <id...>` (or `--all`) to refresh selected sources and lock data.
3. Use `npx docs-cache install` to restore cache/targets from `docs-lock.json` without rewriting the lock file.
4. Use `npx docs-cache sync --frozen` in CI to fail fast when lock data drifts.
4. Use `npx docs-cache sync --frozen` in CI to fail fast when lock data or managed OpenCode references drift.
5. Use `npx docs-cache pin <id...>` only when you explicitly want to rewrite config refs to commit SHAs.

## Configuration
Expand All @@ -68,14 +73,13 @@ Use this flow to keep behavior predictable (similar to package manager manifest
```jsonc
{
"$schema": "https://github.com/fbosch/docs-cache/blob/master/docs.config.schema.json",
"sources": [
{
"id": "framework",
"repo": "https://github.com/framework/core.git",
"ref": "main", // or specific commit hash
"targetDir": "./agents/skills/framework-skill/references", // symlink/copy target
"include": ["guide/**"], // file globs to include from the source
"toc": true, // defaults to "compressed" (for agents)
"sources": [
{
"id": "framework",
"repo": "https://github.com/framework/core.git",
"ref": "main", // or specific commit hash
"include": ["guide/**"], // file globs to include from the source
"toc": true, // defaults to "compressed" (for agents)
},
],
}
Expand All @@ -89,8 +93,36 @@ Use this flow to keep behavior predictable (similar to package manager manifest
| ---------- | -------------------------------------- | -------- |
| `cacheDir` | Directory for cache. Default: `.docs`. | Optional |
| `defaults` | Default settings for all sources. | Optional |
| `opencode` | OpenCode reference-sync decision. | Optional |
| `sources` | List of repositories to sync. | Required |

### OpenCode references

`docs-cache init` and interactive `docs-cache sync` detect project-local `opencode.json` and `opencode.jsonc` files. When a config is detected, `docs-cache` asks whether to sync references and stores the answer:

```jsonc
{
"opencode": true
}
```

Declining stores `"opencode": false` and suppresses future prompts. It stops further management without changing existing OpenCode references. Omit the field only while no decision has been made. To target a specific project-local config, set a manual override such as `"opencode": { "configPath": ".opencode/opencode.jsonc" }`.

`sync` creates or updates managed OpenCode references whose aliases are source IDs. Paths are relative to the OpenCode config file:

```jsonc
{
"references": {
"framework": {
"path": "../.docs/framework",
"description": "Use for documentation from framework/core. Start with TOC.md."
}
}
}
```

The cache remains gitignored. `targetDir`, symlinks, copies, and unwrapped directories are never used for OpenCode reference paths. `docs-cache` preserves user-owned references and fails on an alias collision. A later `sync` removes stale managed aliases after a source is removed. When TOC generation is disabled for a source, its description does not direct OpenCode to `TOC.md`. `sync --frozen` validates references without writing the OpenCode config. Restart OpenCode after references change because it reads configuration at startup.

<details>
<summary>Show default and source options</summary>

Expand All @@ -110,7 +142,7 @@ These fields can be set in `defaults` and are inherited by every source unless o
| `maxFiles` | Maximum total files to materialize. |
| `ignoreHidden` | Skip hidden files and directories (dotfiles). Default: `false`. |
| `allowHosts` | Allowed Git hosts. Default: `["github.com", "gitlab.com", "visualstudio.com"]`. |
| `toc` | Generate per-source `TOC.md`. Default: `true`. Supports `true`, `false`, or a format: `"tree"` (human readable), `"compressed"` |
| `toc` | Generate per-source `TOC.md`. Default: `true` with compressed output. `"tree"` uses headings and links; `false` removes generated TOC files. |
| `unwrapSingleRootDir` | If the materialized output is nested under a single directory, unwrap it (recursively). Default: `true`. |

> Brace expansion in `include` supports comma-separated lists (including multiple groups) like `**/*.{md,mdx}` and is capped at 500 expanded patterns per include entry. It does not support nested braces or numeric ranges.
Expand All @@ -130,7 +162,7 @@ These fields can be set in `defaults` and are inherited by every source unless o
| ----------- | ---------------------------------------------------------------- |
| `targetDir` | Path where files should be symlinked/copied to, outside `.docs`. |

> **Note**: Sources are always downloaded to `.docs/<id>/`. If you provide a `targetDir`; `docs-cache` will create a symlink or copy pointing from the cache to that target directory.
> **Note**: Sources are always downloaded to `.docs/<id>/`. If you provide a `targetDir`, `docs-cache` creates a symlink or copy pointing from the cache to that target directory. It is not enabled by default.

</details>

Expand Down
23 changes: 23 additions & 0 deletions docs.config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,29 @@
},
"additionalProperties": false
},
"opencode": {
"anyOf": [
{
"type": "boolean",
"const": true
},
{
"type": "boolean",
"const": false
},
{
"type": "object",
"properties": {
"configPath": {
"type": "string",
"minLength": 1
}
},
"required": ["configPath"],
"additionalProperties": false
}
]
},
"sources": {
"type": "array",
"items": {
Expand Down
16 changes: 7 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"private": false,
"type": "module",
"version": "0.6.0",
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
"packageManager": "pnpm@11.13.0+sha512.88d94724d8f2e6c186744a5584c6e59ecac869ec7ba15e9cb4cd628e8dc7066820b2481d8ee3b51ea8da323a7378068aa58c556a3720d32b7c20a051d088363a",
"description": "CLI for deterministic local caching of external documentation for agents and tools",
"author": "Frederik Bosch",
"license": "MIT",
Expand Down Expand Up @@ -95,6 +95,11 @@
"types": "./dist/esm/git/*.d.ts",
"default": "./dist/esm/git/*.mjs"
},
"#opencode/*": {
"coverage": "./src/opencode/*.ts",
"types": "./dist/esm/opencode/*.d.ts",
"default": "./dist/esm/opencode/*.mjs"
},
"#types/*": {
"coverage": "./src/types/*.ts",
"types": "./dist/esm/types/*.d.ts",
Expand All @@ -108,23 +113,24 @@
"execa": "^9.6.1",
"fast-glob": "^3.3.3",
"log-update": "7.0.2",
"jsonc-parser": "3.3.1",
"picocolors": "^1.1.1",
"picomatch": "^4.0.4",

Check warning on line 118 in package.json

View workflow job for this annotation

GitHub Actions / precheck

Unused dependency

Package 'picomatch' is listed in dependencies but never imported by this package. Run: pnpm remove picomatch
"zod": "^4.3.6"
},
"devDependencies": {
"@biomejs/biome": "^2.4.11",
"@size-limit/file": "^12.0.1",

Check warning on line 123 in package.json

View workflow job for this annotation

GitHub Actions / precheck

Unused devDependency

Package '@size-limit/file' is listed in devDependencies but never imported by this package. Run: pnpm remove @size-limit/file
"@types/node": "^25.6.0",
"bumpp": "^11.0.1",
"c8": "^10.1.3",
"fallow": "3.3.0",
"jiti": "^2.6.1",

Check warning on line 128 in package.json

View workflow job for this annotation

GitHub Actions / precheck

Dev dependency in production

Package 'jiti' is a devDependency imported by production code at runtime. Move it from devDependencies to dependencies so a production-only install does not break at runtime.
"lint-staged": "^16.4.0",
"simple-git-hooks": "^2.13.1",
"size-limit": "^12.0.1",
"tinybench": "^6.0.0",

Check warning on line 132 in package.json

View workflow job for this annotation

GitHub Actions / precheck

Unused devDependency

Package 'tinybench' is listed in devDependencies but never imported by this package. Run: pnpm remove tinybench
"ts-complex": "^1.0.0",

Check warning on line 133 in package.json

View workflow job for this annotation

GitHub Actions / precheck

Dev dependency in production

Package 'ts-complex' is a devDependency imported by production code at runtime. Move it from devDependencies to dependencies so a production-only install does not break at runtime.
"tsx": "4.23.0",
"typescript": "7.0.2"
},
Expand All @@ -147,13 +153,5 @@
"*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
"biome check --write --no-errors-on-unmatched"
]
},
"pnpm": {
"overrides": {
"defu@<=6.1.4": "6.1.5",
"lodash@>=4.0.0 <=4.17.23": "4.18.0",
"minimatch@>=10.0.0 <10.2.3": "10.2.4",
"picomatch@<2.3.2": "2.3.2"
}
}
}
34 changes: 12 additions & 22 deletions pnpm-lock.yaml

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

7 changes: 7 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
allowBuilds:
esbuild: true
simple-git-hooks: true

overrides:
defu@<=6.1.4: 6.1.5
lodash@>=4.0.0 <=4.17.23: 4.18.0
minimatch@<10.2.1: '>=10.2.1'
minimatch@>=10.0.0 <10.2.3: '>=10.2.3'
picomatch@<2.3.2: 2.3.2
rollup@>=4.0.0 <4.59.0: '>=4.59.0'

Check warning on line 11 in pnpm-workspace.yaml

View workflow job for this annotation

GitHub Actions / precheck

Unused dependency override

Override `rollup@>=4.0.0 <4.59.0` forces `=4.0.0 <4.59.0` to `>=4.59.0` but no workspace package depends on `=4.0.0 <4.59.0`. may target a transitive dependency; pnpm install --frozen-lockfile is the ground truth. Delete the entry, or scope it under a real parent (`pkg>=4.0.0 <4.59.0`) if it pins a transitive.
svgo@=4.0.0: '>=4.0.1'

Check warning on line 12 in pnpm-workspace.yaml

View workflow job for this annotation

GitHub Actions / precheck

Unused dependency override

Override `svgo@=4.0.0` forces `svgo` to `>=4.0.1` but no workspace package depends on `svgo`. may target a transitive dependency; pnpm install --frozen-lockfile is the ground truth. Delete the entry, or scope it under a real parent (`pkg>svgo`) if it pins a transitive.
6 changes: 6 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ export { loadConfig } from "#config";
export { redactRepoUrl } from "#git/redact";
export { enforceHostAllowlist, parseLsRemote } from "#git/resolve-remote";
export { resolveRepoInput } from "#git/resolve-repo";
export { saveOpenCodeConsent } from "#opencode/consent";
export {
detectOpenCodeConfig,
getOpenCodeConfigCandidates,
} from "#opencode/detection";
export { planOpenCodeReferences } from "#opencode/references";
27 changes: 27 additions & 0 deletions src/atomic-write.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { chmod, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
import path from "node:path";

export const writeFileAtomically = async (
filePath: string,
data: string,
options?: { mode?: number },
) => {
let mode = options?.mode ?? 0o644;
try {
mode = (await stat(filePath)).mode;
} catch {
// Use the caller's default mode when creating a new file.
}
await mkdir(path.dirname(filePath), { recursive: true });
const tempPath = path.join(
path.dirname(filePath),
`.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`,
);
try {
await writeFile(tempPath, data, { encoding: "utf8", mode });
await chmod(tempPath, mode);
await rename(tempPath, filePath);
} finally {
await rm(tempPath, { force: true });
}
};
Comment thread
fbosch marked this conversation as resolved.
Loading
Loading