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
38 changes: 29 additions & 9 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,25 @@ on:
tags: ['v*']

permissions:
contents: write
contents: read
id-token: write

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: '20'
node-version: '24'
registry-url: 'https://registry.npmjs.org'
cache: 'npm'
- run: npm install -g npm@11.5.1
- run: npm ci
package-manager-cache: false
- run: npm install -g npm@11.19.0
- name: Check tag matches package version
run: |
TAG_VERSION=${GITHUB_REF#refs/tags/v}
Expand All @@ -28,8 +32,24 @@ jobs:
echo "Version mismatch: tag v$TAG_VERSION, package.json v$PKG_VERSION" >&2
exit 1
fi
- run: npm ci
- run: npm publish
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "$GITHUB_REF_NAME" --title "$GITHUB_REF_NAME" --generate-notes --verify-tag

github-release:
needs: publish
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
steps:
- run: |
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
exit 0
fi
gh release create "$GITHUB_REF_NAME" \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--generate-notes \
--title "$GITHUB_REF_NAME"
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
coverage/
dist/
node_modules/
package-lock.json
4 changes: 4 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"printWidth": 100,
"singleQuote": true
}
74 changes: 41 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# @nbtca/docs

Data-only library for the [NBTCA documents repository](https://github.com/nbtca/documents).
Fetches directory listings and raw markdown files from GitHub with built-in TTL caching,
stale-on-error fallback, and rate-limit handling.

Rendering is the consumer's job (e.g. `@nbtca/prompt`).
Typed GitHub client for the [NBTCA documents repository](https://github.com/nbtca/documents).
It lists Markdown documents, reads raw content, caches successful responses, and falls back to
stale data after transient failures. Rendering remains the consumer's responsibility.

## Install

Expand All @@ -17,53 +15,63 @@ npm install @nbtca/docs
```ts
import { createDocsClient } from '@nbtca/docs';

const docs = createDocsClient(); // defaults to nbtca/documents@main
const docs = createDocsClient();

const items = await docs.listDir('tutorial'); // DocItem[]
const all = await docs.listAll(); // all markdown DocItem[]
const md = await docs.getFile('repair/guide.md'); // string (raw markdown)
```

Custom target:

```ts
const docs = createDocsClient({
owner: 'my-org',
repo: 'my-docs',
branch: 'main',
token: process.env.GITHUB_TOKEN,
});
const sections = await docs.listDir();
const documents = await docs.listAll();
const markdown = await docs.getFile('repair/guide.md');
const page = await docs.getDocument('repair/index.md');
const matches = await docs.search('repair', { pathPrefix: 'repair' });
```

## API

### `createDocsClient(options?)`

| Option | Default | Description |
|---|---|---|
| `owner` | `'nbtca'` | GitHub org/user |
| `repo` | `'documents'` | Repository name |
| `branch` | `'main'` | Branch or ref |
| `token` | `GITHUB_TOKEN` env | Auth token (raises rate limit) |
| `cacheTtlMs.dir` | `300000` (5 min) | Directory listing cache TTL |
| `cacheTtlMs.file` | `600000` (10 min) | File content cache TTL |
| Option | Default | Description |
| ----------------- | ---------------------------- | ---------------------------- |
| `owner` | `'nbtca'` | GitHub owner |
| `repo` | `'documents'` | Repository name |
| `branch` | `'main'` | Branch name or ref |
| `token` | `GITHUB_TOKEN` or `GH_TOKEN` | GitHub token |
| `cacheTtlMs.dir` | `300000` | Directory and tree cache TTL |
| `cacheTtlMs.file` | `600000` | File cache TTL |

### `docs.listDir(path?)`

Returns `DocItem[]` for the given path (root if omitted).
Filters out hidden files, non-markdown files, and repository metadata.
Lists directories and Markdown files at a repository-relative path. The root path is used when
`path` is omitted.

### `docs.getFile(path)`

Returns raw markdown as a string. Falls back to stale cache on network error.
Returns raw file content.

### `docs.listAll()`

Returns every markdown file in the repository through GitHub's recursive tree API.
Lists every Markdown file through GitHub's recursive tree API.

### `docs.listSections()`

Returns top-level content sections with document counts and optional index paths.

### `docs.getDocument(path)`

Returns content with its route, section, title, summary, and semantic component attributes. Component
metadata covers `PageHero`, `FactStrip`, `LinkCard`, `Split`, `TimelineEntry`, and `Figure` without
imposing a renderer.

### `docs.search(query, options?)`

Searches paths, titles, summaries, Markdown text, and semantic component attributes. Results are
ranked and include excerpts. Use `pathPrefix` to scope a search and `limit` to cap results.

### `docs.clear()`

Clears all cached values and in-flight request bookkeeping.

### `DocsFetchError`

Thrown when a fetch fails with no stale cache available. Has `.path` and `.status` fields.
Thrown when a request fails without usable stale data. Exposes `path` and HTTP `status`.

## License

Expand Down
45 changes: 45 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import eslint from '@eslint/js';
import prettier from 'eslint-config-prettier';
import globals from 'globals';
import tseslint from 'typescript-eslint';

export default tseslint.config(
{
ignores: ['coverage', 'dist', 'node_modules'],
},
eslint.configs.recommended,
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
files: ['**/*.ts'],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': [
'error',
{ fixStyle: 'inline-type-imports', prefer: 'type-imports' },
],
'@typescript-eslint/no-import-type-side-effects': 'error',
'@typescript-eslint/switch-exhaustiveness-check': 'error',
},
},
{
files: ['src/__tests__/**/*.ts'],
rules: {
'@typescript-eslint/require-await': 'off',
},
},
{
files: ['**/*.mjs'],
extends: [tseslint.configs.disableTypeChecked],
languageOptions: {
globals: globals.node,
},
},
prettier,
);
Loading