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
5 changes: 5 additions & 0 deletions .changeset/intent-skills-rename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stainless-code/persist": patch
---

Ship Intent skills for the full public surface — core `persist`, `persist-*` (sources/codecs/backends/transport), and `<framework>-persist`. Re-run `npx @tanstack/intent@latest install` if agent config still points at `skills/tanstack-store`.
32 changes: 7 additions & 25 deletions .github/workflows/check-skills.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
# check-skills.yml — Validates intent skills on PRs. After a release or manual
# run, opens or updates one review PR when existing skills, artifact coverage,
# or workspace package coverage need review.
#
# Triggers: pull requests touching skills/artifacts, new release published, or
# manual workflow_dispatch.
#
# Validate skills on PR; post-release/manual: open Intent stale-coverage review PR.
# intent-workflow-version: 3

name: Check Skills
Expand All @@ -14,8 +8,6 @@ on:
paths:
- "skills/**"
- "**/skills/**"
- "_artifacts/**"
- "**/_artifacts/**"
release:
types: [published]
workflow_dispatch: {}
Expand All @@ -31,16 +23,11 @@ jobs:
- name: Checkout
uses: actions/checkout@v7

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 20

- name: Install intent
run: npm install -g @tanstack/intent@0.3.4
- name: Setup Bun and install
uses: ./.github/actions/setup

- name: Validate skills
run: intent validate --github-summary
run: bun run intent:validate -- --github-summary

review:
name: Check intent skill coverage
Expand All @@ -55,18 +42,13 @@ jobs:
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 20

- name: Install intent
run: npm install -g @tanstack/intent@0.3.4
- name: Setup Bun and install
uses: ./.github/actions/setup

- name: Check skills
id: stale
run: |
intent stale --github-review --package-label "@stainless-code/persist"
bun run intent:stale -- --github-review --package-label "@stainless-code/persist"

- name: Open or update review PR
if: steps.stale.outputs.has_review == 'true'
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"store",
"svelte",
"tanstack",
"tanstack-intent",
"tanstack-store",
"typescript",
"valtio",
Expand All @@ -54,8 +55,7 @@
],
"files": [
"dist",
"skills",
"!skills/_artifacts"
"skills"
],
"type": "module",
"sideEffects": false,
Expand Down
19 changes: 13 additions & 6 deletions scripts/sync-skill-versions.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
#!/usr/bin/env bun
// Stamp `library_version` in every skills/*/SKILL.md frontmatter to the
// Stamp `library_version` in every skills/**/SKILL.md frontmatter to the
// current package version. Wired into the `version` script so the changesets
// "Version packages" PR carries the skill bump alongside the package bump —
// keeps `intent stale` quiet without a manual review PR per release.
import { readFileSync, readdirSync, writeFileSync, existsSync } from "node:fs";
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";

const { version } = JSON.parse(readFileSync("package.json", "utf8"));
const FIELD = /^( *library_version:\s*")([^"]*)(")/m;

function* walkSkillFiles(dir: string): Generator<string> {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const nested = join(dir, entry.name);
const skill = join(nested, "SKILL.md");
if (existsSync(skill)) yield skill;
yield* walkSkillFiles(nested);
}
}

let touched = 0;
for (const entry of readdirSync("skills", { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
const file = join("skills", entry.name, "SKILL.md");
if (!existsSync(file)) continue;
for (const file of walkSkillFiles("skills")) {
const before = readFileSync(file, "utf8");
const after = before.replace(FIELD, `$1${version}$3`);
if (after !== before) {
Expand Down
62 changes: 62 additions & 0 deletions skills/alpine-persist/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
name: alpine-persist
description: Gate Alpine.js UI on Persist hydration (Alpine.plugin + useHydrated / $hydrated). Use when wiring HydrationSignal into Alpine data components.
license: MIT
metadata:
type: framework
library: "@stainless-code/persist"
library_version: "0.4.0"
framework: "alpine"
requires:
- persist
sources:
- stainless-code/persist:src/adapters/frameworks/alpine.ts
---

# Alpine hydration gate

`@stainless-code/persist/frameworks/alpine` is **plugin-first**: `Alpine.plugin(persist)` registers `$hydrated` and enables reactive `useHydrated`. Call `destroy()` from `Alpine.data` teardown.

## Install

```bash
bun add @stainless-code/persist alpinejs
```

Peer: `alpinejs` `>=3.0.0` (types via `@types/alpinejs` — package ships none).

## Minimal wiring

```ts
import Alpine from "alpinejs";
import persist, {
useHydrated,
} from "@stainless-code/persist/frameworks/alpine";

Alpine.plugin(persist);

Alpine.data("prefs", () => {
const hydration = useHydrated(prefsHydration);
return {
get hydrated() {
return hydration.hydrated;
},
destroy() {
hydration.destroy();
},
};
});
```

Template: `x-show="$hydrated(prefsHydration).hydrated"` — `$hydrated(signal)` returns a **bag** (`{ hydrated, destroy }`), not a boolean (a bare bag is always truthy in `x-show`). Magic caches per element.

## Common mistakes

- **Skipping `Alpine.plugin(persist)`.** `useHydrated` falls back to a plain object + one-time non-prod warn.
- **`x-show="$hydrated(...)"` without `.hydrated`.**
- **Not forwarding `destroy()`** from `Alpine.data`.

## API surface

- default/`persist(Alpine)` — plugin; registers `$hydrated(signal) → HydratedBag`
- `useHydrated(signal) → { hydrated; destroy() }` (null signal → `{ hydrated: true }`)
54 changes: 54 additions & 0 deletions skills/angular-persist/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
name: angular-persist
description: Gate Angular UI on Persist hydration with useHydrated (readonly Signal). Call inside an injection context; use when avoiding flash of default state on async backends.
license: MIT
metadata:
type: framework
library: "@stainless-code/persist"
library_version: "0.4.0"
framework: "angular"
requires:
- persist
sources:
- stainless-code/persist:src/adapters/frameworks/angular.ts
---

# Angular hydration gate

`@stainless-code/persist/frameworks/angular` returns a readonly `Signal<boolean>`. A real signal needs an **injection context** (`effect()`); null/undefined returns a shared `signal(true)` without `effect()`.

## Install

```bash
bun add @stainless-code/persist @angular/core
```

Peer: `@angular/core` `>=17.0.0`.

## Minimal wiring

```ts
import { useHydrated } from "@stainless-code/persist/frameworks/angular";

export class PrefsComponent {
hydrated = useHydrated(prefsHydration);
}
```

```html
@if (hydrated()) {
<prefs-panel />
} @else {
<skeleton />
}
```

## Common mistakes

- **Calling outside injection context.**
- **Treating the signal as store state.**
- **SSR / null signal** — both surface as hydrated `true`.

## API surface

- `useHydrated(signal) → Signal<boolean>` (readonly)
53 changes: 53 additions & 0 deletions skills/lit-persist/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
name: lit-persist
description: Gate Lit element UI on Persist hydration with HydrationController. Use when wiring HydrationSignal into a ReactiveControllerHost; not a hook.
license: MIT
metadata:
type: framework
library: "@stainless-code/persist"
library_version: "0.4.0"
framework: "lit"
requires:
- persist
sources:
- stainless-code/persist:src/adapters/frameworks/lit.ts
---

# Lit hydration gate

`@stainless-code/persist/frameworks/lit` exports `HydrationController` — a `ReactiveController`, not a hook. Constructor calls `host.addController(this)`; subscribes on `hostConnected`, tears down on `hostDisconnected`.

## Install

```bash
bun add @stainless-code/persist lit
```

Peer: `lit` `>=3.0.0`.

## Minimal wiring

```ts
import { LitElement, html } from "lit";
import { HydrationController } from "@stainless-code/persist/frameworks/lit";

class PrefsEl extends LitElement {
#hydration = new HydrationController(this, prefsHydration);

render() {
return this.#hydration.hydrated
? html`<prefs-panel></prefs-panel>`
: html`<skeleton-el></skeleton-el>`;
}
}
```

## Common mistakes

- **Inventing `useHydrated` for Lit.** Use the controller.
- **Caching `hydrated` outside render** — the controller already `requestUpdate`s on connect/change; read the getter in `render()`.
- **Null signal ≠ loading** → `hydrated` true, no subscribe.

## API surface

- `new HydrationController(host, signal)` — getter `hydrated: boolean`
48 changes: 48 additions & 0 deletions skills/persist-async-storage/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: persist-async-storage
description: Persist with React Native AsyncStorage via @stainless-code/persist/backends/async-storage. Use for RN string-wire JSON state; always gate with useHydrated.
license: MIT
metadata:
type: composition
library: "@stainless-code/persist"
library_version: "0.4.0"
framework: "@react-native-async-storage/async-storage"
requires:
- persist
sources:
- stainless-code/persist:src/adapters/backends/async-storage.ts
---

# AsyncStorage backend

JSON via `createJSONStorage` under the hood. Fully **async** → **`useHydrated` mandatory**. Factory does **not** accept `clearCorruptOnFailure` — use `createStorage(() => asyncStorageStateStorage(), jsonCodec(), opts)` when you need it.

## Install

```bash
bun add @stainless-code/persist @react-native-async-storage/async-storage
```

Peer: `@react-native-async-storage/async-storage` `>=1.0.0`.

## Minimal wiring

```ts
import { createAsyncStorage } from "@stainless-code/persist/backends/async-storage";

const storage = createAsyncStorage<Prefs>()!;
```

Optional custom instance for namespacing: `createAsyncStorage(myAsyncStorage)`.

## Common mistakes

- **Treating hydrate as sync.**
- **Expecting Set/Map/Date** — use seroval compose or avoid.
- **Passing `clearCorruptOnFailure` to the factory.**

## API surface

- `createAsyncStorage(storage?)` · `asyncStorageStateStorage(storage?)`

See also: `persist-mmkv` (sync RN).
45 changes: 45 additions & 0 deletions skills/persist-compressed/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
name: persist-compressed
description: Compress a string-wire StateStorage with @stainless-code/persist/backends/compressed (gzip/deflate). Backend wrapper — compose with createStorage; stack compress-then-encrypt.
license: MIT
metadata:
type: composition
library: "@stainless-code/persist"
library_version: "0.4.0"
requires:
- persist
sources:
- stainless-code/persist:src/adapters/backends/compressed.ts
---

# Compressed backend wrapper

**Not a `StorageCodec`.** Uses `CompressionStream` / `DecompressionStream` (default `gzip`); output is **base64** on the string wire. Returns `undefined` if streams unavailable. Documented stack with encryption: **compress → encrypt**.

## Minimal wiring

```ts
import { createStorage, jsonCodec } from "@stainless-code/persist";
import { createCompressedStorage } from "@stainless-code/persist/backends/compressed";

const storage = createStorage<Prefs>(
() => createCompressedStorage(() => localStorage)!,
jsonCodec(),
);
```

Richer graphs → `persist-seroval` as the codec.

Formats: `gzip` | `deflate` | `deflate-raw` (`deflate-raw` needs Node `20.12+`).

## Common mistakes

- **Using as a codec.**
- **Encrypt-then-compress.**
- **Assuming streams exist in every runtime.**

## API surface

- `createCompressedStorage(getStorage, { format? }) → StateStorage<string> | undefined`

See also: `persist-encrypted`.
Loading