chs: re-add drifted files

dot_agents/dot_skill-lock.json
This commit is contained in:
Prad Nukala committed 2026-08-14 13:53:55 -04:00
1 parent 03ef5c94ea
commit 42d17e825e
30 files changed
+230 -145

No files matched your search

+2 -2
View File
@@ -44,8 +44,8 @@ The single biggest shift for most sources — especially Mintlify — is that **
- **Folders become groups, files become pages.** A page's sidebar label is its frontmatter `title`; a group's label is the humanized folder name.
- **Ordering resolves highest-priority-first:** an explicit `navigation.sidebar` (replaces the whole tree) → a folder's `meta.ts` `pages` array → a page's frontmatter `sidebar.order` → the filesystem (`index` first, then numeric filename prefix like `01-`, then alphabetical).
- **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages })`). The `pages` array lists children by slug (numeric prefix and parentheses stripped); children you omit fall back to their own `sidebar.order`, then filesystem order (`index` still sorts first) — so a partial `pages` list is safe, but list every child when the source declared a complete order.
- **Sidebar render mode is global, not per-folder.** `navigation.sidebar.display` in `blume.config.ts` is `"flat"` (default), `"group"` (collapsible), or `"page"` (drill-in sub-panel) and applies to **every** group at once. (It used to live on each folder's `meta.ts` as `display`; that field was **removed** — writing it in a `meta.ts` is now a build error. Set it once in config instead.) An explicit `navigation.sidebar` item may still override its own group's `display`.
- **`meta.ts` refines one folder** (`defineMeta({ title, icon, order, collapsed, pages, display })`). The `pages` array lists children by slug (numeric prefix and parentheses stripped); children you omit fall back to their own `sidebar.order`, then filesystem order (`index` still sorts first) — so a partial `pages` list is safe, but list every child when the source declared a complete order.
- **Sidebar render mode: a global default with per-folder overrides.** `navigation.sidebar.display` in `blume.config.ts` is `"flat"` (default), `"group"` (collapsible), or `"page"` (drill-in sub-panel) and sets the mode for every group at once. A folder can override its **own** group: `display` in its `meta.ts`, or — sugar when the folder has an `index` page — `sidebar.display` in that index page's frontmatter. Precedence: index frontmatter → `meta.ts` → global config → `flat`; an override applies to that one group only (nested subgroups resolve their own chain). So a source's per-category collapse/drill-in modes migrate per folder — only reach for the global mode when the whole sidebar changes. Under an explicit `navigation.sidebar`, the config item's own `display` field is the only per-group control (frontmatter/meta `display` is ignored there, with a `BLUME_SIDEBAR_DISPLAY_IGNORED` warning).
- **An explicit `navigation.sidebar` replaces filesystem generation entirely.** Use it only for a nav shape files can't express. Its items are a page route string, a group (`{ label, items }`), or a link (`{ label, href }`).
- **Config-declared nesting has no on-disk counterpart — materialize it or it flattens silently.** When a source (Mintlify `groups`, Nextra `_meta`, a Docusaurus sidebar…) declares a nested group, its pages usually sit **flat in one folder** and the grouping lives only in config. Filesystem-derived nav sees the flat folder and drops the inner group. To keep the nesting you must **either** move those pages into a real subfolder (`meta.ts` for label/`collapsed`) — which changes their URLs, so add `redirects` — **or** declare the group in an explicit `navigation.sidebar`, which nests the existing routes without moving a file. Walk config `pages`/nav arrays **recursively** during inventory and record where config nesting depth exceeds on-disk depth; that gap is exactly what gets lost.
@@ -41,7 +41,7 @@ Every Docusaurus repo serves **`static/`** at the site root (`static/img/foo.png
## Navigation: `sidebars.js` + `_category_.json`
- **Autogenerated sidebar** (`{ type: 'autogenerated', dirName: '...' }`) → Blume's default filesystem navigation. Docusaurus strips numeric prefixes (`01-`) exactly like Blume, so the convention round-trips; no config needed.
- **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed` (**no `display` field in `meta.ts`** — if collapse matters, set the global `navigation.sidebar.display: "group"` once in config).
- **Explicit sidebar** (arrays of doc IDs, categories, links) → restructure into folders where possible; use `navigation.sidebar` only for shapes files can't express. A category `{ type: 'category', label, items }` → a folder (label → `meta.ts` `title`); `collapsed` → `meta.ts` `collapsed`, and collapsible rendering → that folder's `meta.ts` `display: "group"` (or `navigation.sidebar.display: "group"` once in config when every category collapses).
- **`_category_.json`** (also `.yml`) (`{ label, position, collapsed, collapsible, link, className, customProps }`) → a folder `meta.ts`: `label`→`title`, `position`→`order`, `collapsed`→`collapsed`. `link.type: 'generated-index'` → an `index` page in the folder — **and the old URL was `/docs/category/<slug>`**, so add a redirect and rewrite inbound links. `link.type: 'doc'` → make that doc the folder's `index`. `collapsible`/`className`/`customProps` → drop (report).
- **`src/pages/` — inventory, don't nuke.** Nearly every repo has a React landing page (`src/pages/index.tsx`) and often extra Markdown pages. Markdown pages → content pages; the React home page → rebuild as a docs index or a custom `.astro` page under `content.pages` — report either way.
@@ -39,7 +39,7 @@ Everything else is `defineConfig({ title })`.
| `description` | **drop** (folders have no description) |
| `collapsible: false` | **drop** (report — no per-folder equivalent) |
`meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages` — there is **no `display` field** (writing one is a build error). Collapse rendering is global: when any folder needs collapsing, set `navigation.sidebar.display: "group"` once in `blume.config.ts`.
`meta.ts` accepts **only** `title`, `icon`, `order`, `collapsed`, `pages`, `display`. Render mode is per-folder or global: a folder that needs collapsible rendering sets its own `meta.ts` `display: "group"` (drill-in is `"page"`); when the whole sidebar should collapse, set `navigation.sidebar.display: "group"` once in `blume.config.ts` instead of repeating it per folder.
**`root: true` folders are Fumadocs' tab mechanism** — Fumadocs UI renders them as layout tabs and scopes the sidebar to the active one. That is exactly Blume's `navigation.tabs`: add `{ label, path, icon? }` per root folder (label/icon from its `meta.json`), pointing at the folder's route. Blume then scopes the sidebar by URL prefix the same way. Don't try to model root folders inside `meta.ts`.
@@ -83,7 +83,7 @@ The codemod touches **only frontmatter**. Icons in MDX **body** (`<Icon icon="
Mintlify's `navigation` object (`tabs`/`anchors`/`dropdowns`/`products`/`versions`/`languages`/`groups`/`pages`) is fully config-declared. **Prefer restructuring content into folders**, not porting the config verbatim:
- **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — is **not** a `meta.ts` field; it's the global `navigation.sidebar.display` in `blume.config.ts`.)
- **`groups`** (`{ group, pages: [...] }`) → a folder per group. The `group` name → the folder's humanized name or a `meta.ts` `title`. Nested groups → nested folders. `expanded: false` → `meta.ts` `collapsed: true` (inverted). `tag` → the folder/page `sidebar.badge`. **Folder metadata is a `meta.ts` module** (`export default defineMeta({ title, icon, order, collapsed, pages, display })`), a TypeScript file — **not** JSON. Blume does not read a `meta.json`, so any folder-level config you carry over from the source (or hand-write to preserve a group's label/order/collapse) must be authored as `meta.ts`. (Render mode — flat/group/page — defaults to the global `navigation.sidebar.display` in `blume.config.ts`; a folder overrides its own group with `meta.ts` `display`, or `sidebar.display` in its `index` page's frontmatter.)
- **Config-only nested groups don't exist on disk — you must materialize them, or they flatten silently.** A nested `{ group, pages }` almost never has a matching subfolder: its pages sit **flat in the parent directory** (e.g. `platform/analytics/getting-started.mdx`, `…/quick-reference.mdx`) and the grouping lives **only** in the `docs.json` `pages` array. If you leave the files where they are, filesystem-derived nav sees one flat folder and the inner group vanishes — Mintlify's `Analytics → Reference → {…}` becomes a flat `Analytics → {…}`. To preserve it you must **either** move those pages into a real subfolder (`platform/analytics/reference/`, with a `meta.ts` for the label/`collapsed`), **or** declare the shape in an explicit `navigation.sidebar`. Walk **every** `pages` array recursively and treat any nested `group` object as a folder-move to plan, not files already in place. When inventorying the nav (workflow step 2), record the config nesting depth separately from the on-disk depth — they diverge exactly here.
- **This is the canonical case for `navigation.sidebar` over folders.** Materializing a purely-presentational nested group as a subfolder changes URLs (`/platform/analytics/getting-started` → `/platform/analytics/reference/getting-started`) for no reason other than a visual grouping, forcing a `redirects` entry per page. When you want to keep the nesting **and** the URLs, an explicit `navigation.sidebar` group (`{ label, items }`) is the better trade — it nests the existing routes without moving any file. Pick per group; don't reflexively flatten.
- **`pages`** entries are page refs (paths without extension) → files at the corresponding path. An entry that's `"GET /path"` is an OpenAPI endpoint stub → **delete it** (Blume generates these; see OpenAPI).
@@ -44,3 +44,4 @@ Include these sections in the PR body or no-op summary:
- Verification run (commands and results)
- Skipped checks, with reasons
- Remaining risk or follow-up
# chs-e2e-test
+9 -2
View File
@@ -176,7 +176,14 @@ Validate spec structure and auto-fix common issues:
import { validateSpec, autoFixSpec } from "@json-render/core";
const { valid, issues } = validateSpec(spec);
const fixed = autoFixSpec(spec);
// issues include: missing_child, invalid_visible (malformed conditions),
// repeat_without_children, repeat_state_mismatch (statePath not an array in state)
const { spec: fixed, fixDetails } = autoFixSpec(spec);
// fixDetails entries are { message, lossy }. Lossless fixes relocate
// misplaced fields; lossy fixes prune dangling children references.
// In a repair loop, withhold lossy fixes until retries are exhausted:
const attempt = autoFixSpec(spec, { lossy: retriesExhausted });
```
## Visibility Conditions
@@ -253,7 +260,7 @@ The `StateStore` interface: `get(path)`, `set(path, value)`, `update(updates)`,
| `diffToPatches` | Generate RFC 6902 JSON Patch operations from object diff |
| `EditMode` | Type: `"patch" \| "merge" \| "diff"` |
| `validateSpec` | Validate spec structure |
| `autoFixSpec` | Auto-fix common spec issues |
| `autoFixSpec` | Auto-fix common spec issues; classifies fixes lossy/lossless, `{ lossy: false }` withholds pruning |
| `createSpecStreamCompiler` | Stream JSONL patches into spec |
| `createJsonRenderTransform` | TransformStream separating text from JSONL in mixed streams |
| `parseSpecStreamLine` | Parse single JSONL line |
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: domain-modeling
description: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model.
description: Build and sharpen a project's domain model. Use when discussing codebase terminology, writing or editing a CONTEXT.md, or recording or editing an ADR.
---
# Domain Modeling
@@ -40,15 +40,20 @@ Description:
Options:
-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
-t, --title <title> - Document title (required)
-c, --content <content> - Markdown content (inline)
-f, --content-file <path> - Read content from file
--project <project> - Attach to project (UUID, slug ID, or name)
--issue <issue> - Attach to issue (identifier like TC-123)
--icon <icon> - Document icon (emoji)
-i, --interactive - Interactive mode with prompts
-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
-t, --title <title> - Document title (required)
-c, --content <content> - Markdown content (inline)
-f, --content-file <path> - Read content from file
--project <project> - Attach to project (UUID, slug ID, or name)
--issue <issue> - Attach to issue (identifier like TC-123)
--initiative <initiative> - Attach to initiative (UUID, slug ID, or name)
--team <team> - Attach to team (key); with --cycle, scopes the cycle lookup instead
--cycle <cycle> - Attach to cycle: name, number, 'active'/'now', 'next', 'previous', or a relative
offset like +1 (team from --team or config)
--release <release> - Attach to release (UUID, name, or version)
--icon <icon> - Document icon (emoji)
-i, --interactive - Interactive mode with prompts
```
### delete
@@ -85,12 +90,17 @@ Description:
Options:
-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
--project <project> - Filter by project (slug or name)
--issue <issue> - Filter by issue (identifier like TC-123)
--json - Output as JSON
--limit <limit> - Limit results (Default: 50)
-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
--project <project> - Filter by project (UUID, slug ID, or name)
--issue <issue> - Filter by issue (identifier like TC-123)
--initiative <initiative> - Filter by initiative (UUID, slug ID, or name)
--team <team> - Filter by team (key); with --cycle, scopes the cycle lookup instead
--cycle <cycle> - Filter by cycle: name, number, 'active'/'now', 'next', 'previous', or a relative
offset like +1 (team from --team or config)
--release <release> - Filter by release (UUID, name, or version)
--json - Output as JSON
--limit <limit> - Limit results (Default: 50)
```
### update
@@ -106,15 +116,21 @@ Description:
Options:
-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
-t, --title <title> - New title for the document
-c, --content <content> - New markdown content (inline)
-f, --content-file <path> - Read new content from file
--icon <icon> - New icon (emoji)
--project <project> - Attach to project (UUID, slug ID, or name)
-e, --edit - Open current content in $EDITOR for editing
--force - Update content even when document comments may lose inline anchors
-h, --help - Show this help.
--workspace <slug> - Target workspace (uses credentials)
-t, --title <title> - New title for the document
-c, --content <content> - New markdown content (inline)
-f, --content-file <path> - Read new content from file
--icon <icon> - New icon (emoji)
--project <project> - Re-point to project (UUID, slug ID, or name); replaces the current attachment
--issue <issue> - Re-point to issue (identifier like TC-123); replaces the current attachment
--initiative <initiative> - Re-point to initiative (UUID, slug ID, or name); replaces the current attachment
--team <team> - Re-point to team (key); with --cycle, scopes the cycle lookup instead
--cycle <cycle> - Re-point to cycle: name, number, 'active'/'now', 'next', 'previous', or a
relative offset like +1 (team from --team or config)
--release <release> - Re-point to release (UUID, name, or version); replaces the current attachment
-e, --edit - Open current content in $EDITOR for editing
--force - Update content even when document comments may lose inline anchors
```
### view
+14 -12
View File
@@ -84,19 +84,19 @@ Use these root directories consistently:
- `app/` for runtime application code
- `db/` for migrations and local database files
- `public/` for static assets served as-is
- root `public/` for static assets served as-is from the app root
- `test/` for shared helpers, fixtures, and integration coverage
- `tmp/` for uploads, caches, local session files, and other scratch data
Inside `app/`, organize by responsibility:
- `assets/` for client entrypoints and client-owned browser behavior
- `actions/` for controller-owned route handlers, route-local response rendering, and route-local UI/helpers that are not shared across route areas
- `data/` for schema, queries, persistence setup, migrations, and runtime data initialization
- `middleware/` for request lifecycle concerns such as auth, sessions, uploads, and database injection
- `public/` directories inside the narrowest owner for browser-reachable source code, with the browser runtime entrypoint at `app/actions/public/entry.ts`
- `ui/` for shared cross-route UI primitives
- `utils/` only for genuinely cross-layer helpers that do not clearly belong elsewhere
- `routes.ts` for the route contract
- `routes.ts` for the shared server-and-browser route contract and type-safe href generation
- `router.ts` for router setup and wiring
### Placement Precedence
@@ -148,10 +148,11 @@ When code could live in multiple places:
- Model HTTP behavior explicitly. Status codes, headers, redirects, cache rules, and content types are part of the route contract
- Make the server route correct first. A POST should already return the right HTML, redirect, or error response on its own before `clientEntry(...)` layers interactivity on top
- Validate input at the boundary using `remix/data-schema` (and `remix/data-schema/form-data` for forms). `parseSafe` makes the failure path a return value instead of an exception
- Derive `AppContext` from the middleware stack so `get(Database)`, `get(Session)`, `get(Auth)`, and similar keys stay typed. If the controller never reads from context, it doesn't need the harness
- Derive `AppContext` from the middleware stack so `get(databaseContext)`, `get(Session)`, `get(Auth)`, and similar keys stay typed. If the controller never reads from context, it doesn't need the harness
- Outside actions and controllers, only use `getContext()` when `asyncContext()` is in the middleware stack
- Remix Component is not React: write `function Name(handle: Handle<Props>) { return () => ... }`, read props from `handle.props`, keep state in setup-scope variables, call `handle.update()` explicitly, and do DOM-sensitive work in event handlers or `queueTask(...)`, not in render
- Prefer host-element mixins via `mix={mixin(...)}` for behavior and styling instead of inventing custom host prop conventions. Use `mix={[...]}` only when composing multiple mixins
- Keep short, one-off static styles inline with `mix={css(...)}`. Extract a module-scoped style descriptor when it forms a reused visual recipe, has substantial selectors, media queries, or keyframes, or is large enough to obscure the component. Export a style descriptor only after multiple modules need the same visual recipe; otherwise keep it with its narrowest owner
- Hydrated `clientEntry(...)` props must be serializable. Do not pass functions, class instances, or opaque runtime objects
## Security And Session Defaults
@@ -227,12 +228,12 @@ Use this map to find the right package quickly. Each entry says what the package
- `remix/data-schema/coerce` — coercion helpers for strings, numbers, booleans, dates, and ids. Use when input arrives as a string but should be a typed value
- `remix/data-schema/form-data` — `f.object` and `f.field` for parsing `FormData` directly. Use in actions that read browser forms
- `remix/data-schema/lazy` — recursive or mutually-referential schemas. Use when a schema needs to refer to itself or another schema that is declared later
- `remix/data-table` — typed tables and a `Database` interface. Use for `table`, `column`, `createDatabase` when modeling persisted data
- `remix/data-table/sqlite`, `remix/data-table/postgres`, `remix/data-table/mysql` — adapters. Use to back `createDatabase` with a real engine. SQLite accepts Node, Bun, and compatible synchronous clients with the shared `prepare`/`exec` surface
- `remix/data-table/migrations` — migration authoring and runners. Use for `createMigration`, `createMigrationRunner`
- `remix/data-table` — typed tables and the shared `Database` API. Use `table` and `column` when modeling persisted data, then create a concrete database from the matching dialect package. Integration packages can implement `DatabaseDriver` and extend `Database` to add another SQL dialect
- `remix/data-table/sqlite`, `remix/data-table/postgres`, `remix/data-table/mysql` — concrete database integrations. Use `createSqliteDatabase`, `createPostgresDatabase`, or `createMysqlDatabase`. SQLite accepts Node, Bun, and compatible synchronous clients with the shared `prepare`/`exec` surface
- `remix/data-table/migrations` — migration authoring and registries. Use for `createMigration` and `createMigrationRegistry`; run migrations with `Database.migrate()`
- `remix/data-table/migrations/node` — `loadMigrations` from disk. Use in startup scripts that apply migrations
- `remix/data-table/operators` — query operators such as `inList(...)`. Use when `where` clauses need set or comparison logic
- `remix/data-table/sql-helpers` — SQL helper utilities for adapter or advanced query work. Avoid this in normal app code unless you are intentionally working below the table/query API
- `remix/data-table/sql-helpers` — SQL helper utilities for database integrations or advanced query work. Avoid this in normal app code unless you are intentionally working below the table/query API
### Auth, Sessions, and Cookies
@@ -318,17 +319,18 @@ export const routes = route({
```typescript
import { createController } from 'remix/router'
import { databaseContext } from '../middleware/database.ts'
import { routes } from '../routes.ts'
export default createController(routes.books, {
actions: {
async index({ get }) {
let db = get(Database)
let db = get(databaseContext)
let allBooks = await db.findMany(books, { orderBy: ['id', 'asc'] })
return render(<BooksIndexPage allBooks={allBooks} />)
},
async show({ get, params }) {
let db = get(Database)
let db = get(databaseContext)
let book = await db.findOne(books, { where: { slug: params.slug } })
if (!book) return new Response('Not Found', { status: 404 })
return render(<BookShowPage book={book} />)
@@ -393,8 +395,8 @@ import { redirect } from 'remix/response/redirect'
import * as s from 'remix/data-schema'
import * as f from 'remix/data-schema/form-data'
import { Session } from 'remix/session'
import { Database } from 'remix/data-table'
import { databaseContext } from '../middleware/database.ts'
import { routes } from '../routes.ts'
let bookSchema = f.object({
@@ -410,7 +412,7 @@ export default createController(routes.books, {
return render(<NewBookPage errors={parsed.issues} />, { status: 400 })
}
let db = get(Database)
let db = get(databaseContext)
let book = await db.create(books, parsed.value)
let session = get(Session)
@@ -8,13 +8,13 @@ How to serve browser scripts and styles from source. Read this when the task inv
- Choosing between `staticFiles()` for already-built files and `createAssetServer()` for source assets that need import rewriting, preloads, or fingerprinted URLs
- Generating script URLs or `<link rel="modulepreload">` tags for a client entry
- Enabling browser HMR for source-served modules
- Keeping server-only files out of the browser via `denyFiles` rules
- Keeping files such as tests out of the browser via `denyFiles` rules
For routing the URL namespace itself, see `routing-and-controllers.md`. For client entry hydration and browser update handling, see `hydration-frames-navigation.md`. For the Node HMR runner and browser HMR channel, see `middleware-and-server.md`.
## When To Reach For It
Use `remix/assets` when the app serves browser JavaScript, TypeScript, or CSS from source files. This is the right tool for client entrypoints, browser-only helpers, styles under `app/assets/`, and monorepo code that should be compiled and served under a public URL namespace.
Use `remix/assets` when the app serves browser JavaScript, TypeScript, or CSS from source files. This is the right tool for client entrypoints, browser-only helpers, styles, and monorepo code that should be compiled and served under a public URL namespace.
Use `staticFiles()` for files that already exist on disk exactly as they should be served. Use `createAssetServer()` for source scripts or styles that need rewriting, dependency scanning, preloads, sourcemaps, or fingerprinted URLs.
@@ -36,9 +36,9 @@ let assetServer = createAssetServer({
'app/*path': 'app/*path',
'node_modules/*path': 'node_modules/*path',
},
allowFiles: ['app/assets/**'],
allowFiles: ['app/routes.ts', 'app/**/public/**'],
allowPackages: ['remix'],
denyFiles: ['app/**/*.server.*'],
denyFiles: ['app/**/*.test.*'],
target: { es: '2020', chrome: '109', safari: '16.4' },
sourceMaps: process.env.NODE_ENV === 'development' ? 'external' : undefined,
minify: process.env.NODE_ENV === 'production',
@@ -61,10 +61,12 @@ export default createController(routes, {
## Rules
- Treat `allowFiles`/`allowPackages` and `denyFiles` as the security boundary for browser-reachable source files.
- Put browser-reachable app source in a `public/` directory inside `app/`, beside its narrowest owner, such as `app/ui/public/` or `app/actions/cart/public/`.
- Every local dependency in a browser module graph must match `allowFiles`, so keep the whole graph inside those `public/` directories. `app/routes.ts` is allowed separately so browser modules can build type-safe links with `routes.*.href(...)`.
- Deny test modules with `denyFiles` so tests can be colocated inside a `public/` directory without becoming browser-reachable.
- Use `allowFiles` and `denyFiles` for file paths and globs. Relative values resolve from `rootDir`.
- Use `allowPackages` for exact package names, not globs or subpaths. Packages allowed by `allowPackages` also allow their installed `dependencies` and `optionalDependencies`; peer dependencies must be listed explicitly if they should be browser-reachable.
- `denyFiles` takes precedence over both file and package allow rules.
- Add `denyFiles` rules for server-only modules such as `*.server.*`, private config, or other files that should never be exposed.
- Set `rootDir` explicitly in monorepos so relative paths resolve from the intended project root.
- `basePath` is the public URL namespace handled by the asset server.
- `fileMap` keys are URL patterns relative to `basePath`, and values are root-relative file path patterns. They use `route-pattern` syntax on both sides.
@@ -76,8 +78,8 @@ export default createController(routes, {
Use `getHref()` when you need the public URL for one module, and `getPreloads()` when you want `<link rel="modulepreload">` tags or `Link` headers for one or more entrypoints and their dependencies.
```typescript
let entryHref = await assetServer.getHref('app/assets/entry.ts')
let preloads = await assetServer.getPreloads(['app/assets/entry.ts'])
let entryHref = await assetServer.getHref('app/actions/public/entry.ts')
let entryPreloads = await assetServer.getPreloads('app/actions/public/entry.ts')
```
Use this when rendering documents or layouts that boot browser behavior with a known client entry.
@@ -116,7 +118,8 @@ const isHmr = Boolean(isDevelopment && process.env.REMIX_NODE_HMR)
const assetServer = createAssetServer({
basePath: '/assets',
fileMap: { '/app/*path': 'app/*path' },
allowFiles: ['app/assets/**'],
allowFiles: ['app/routes.ts', 'app/**/public/**'],
denyFiles: ['app/**/*.test.*'],
watch: isDevelopment,
hmr: isHmr
? async () => (await import('remix/node-hmr/runtime')).createBrowserHmrChannel()
@@ -137,7 +137,7 @@ Notice that there is no manual `Set-Cookie` plumbing in the action — the sessi
```typescript
import { auth, createSessionAuthScheme } from 'remix/middleware/auth'
import { Session } from 'remix/session'
import { Database } from 'remix/data-table'
import { databaseContext } from '~/middleware/database.ts'
export function loadAuth() {
return auth({
@@ -148,7 +148,7 @@ export function loadAuth() {
return data ?? null
},
async verify(value, context) {
let db = context.get(Database)
let db = context.get(databaseContext)
return (await db.find(users, value.userId)) ?? null
},
invalidate(session) {
@@ -195,7 +195,7 @@ export let passwordProvider = createCredentialsAuthProvider({
return s.parse(loginSchema, formData)
},
async verify({ email, password }, context) {
let db = context.get(Database)
let db = context.get(databaseContext)
let user = await db.findOne(users, { where: { email } })
if (!user || !(await verifyPassword(password, user.password_hash))) {
return null
@@ -300,7 +300,7 @@ export default createController(routes.auth.google, {
async callback(context) {
let { result, returnTo } = await finishExternalAuth(googleProvider, context)
let db = context.get(Database)
let db = context.get(databaseContext)
let { user, authAccount } = await resolveExternalAuth(db, result)
let session = completeAuth(context)
@@ -322,7 +322,7 @@ Use `refreshExternalAuth(provider, tokens)` when an app has stored OAuth/OIDC to
```typescript
async function refreshGoogleTokens({ get }) {
let db = get(Database)
let db = get(databaseContext)
let account = await db.findOne(authAccounts, { where: { provider: 'google' } })
if (!account) return null
@@ -113,30 +113,30 @@ export const books = table({
## Database Setup
Create a database with an adapter and expose it via middleware:
Create a database and expose it via middleware:
```typescript
import BetterSqlite3 from 'better-sqlite3'
import { createDatabase, Database } from 'remix/data-table'
import { createSqliteDatabaseAdapter } from 'remix/data-table/sqlite'
import { createSqliteDatabase } from 'remix/data-table/sqlite'
let sqlite = new BetterSqlite3('./db/app.db')
sqlite.pragma('foreign_keys = ON')
let adapter = createSqliteDatabaseAdapter(sqlite)
export let db = createDatabase(adapter)
export let db = createSqliteDatabase(sqlite)
```
`createSqliteDatabaseAdapter` accepts synchronous SQLite clients with a shared `prepare`/`exec` surface, including Node's `node:sqlite`, Bun's `bun:sqlite`, and compatible clients. Use whichever client fits the runtime instead of assuming `better-sqlite3` is required.
`createSqliteDatabase` accepts synchronous SQLite clients with a shared `prepare`/`exec` surface, including Node's `node:sqlite`, Bun's `bun:sqlite`, and compatible clients. Use whichever client fits the runtime instead of assuming `better-sqlite3` is required.
### Database middleware
```typescript
import type { Middleware } from 'remix/router'
import { Database } from 'remix/data-table'
import type { Database } from 'remix/data-table'
import { createContextKey, type Middleware } from 'remix/router'
export const databaseContext = createContextKey<Database>()
export function loadDatabase(): Middleware {
return async (context, next) => {
context.set(Database, db)
context.set(databaseContext, db)
return next()
}
}
@@ -145,7 +145,7 @@ export function loadDatabase(): Middleware {
### Querying
```typescript
let db = get(Database)
let db = get(databaseContext)
// Find by primary key
let book = await db.find(books, id)
@@ -220,7 +220,7 @@ Do **not** import app code (e.g. `app/data/schema.ts`) into migration files. Mig
### Transaction modes
Migrations run inside a transaction by default (when the adapter supports transactional DDL). Override per migration with a directive comment in `up.sql`:
Migrations run inside a transaction by default (when the database supports transactional DDL). Override per migration with a directive comment in `up.sql`:
```sql
-- data-table/transaction: none
@@ -232,15 +232,15 @@ Modes: `auto` (default — wrap when supported), `required` (wrap; throw if unsu
### Running migrations
```typescript
import { createMigrationRunner } from 'remix/data-table/migrations'
import { loadMigrations } from 'remix/data-table/migrations/node'
let migrations = await loadMigrations('./db/migrations')
let runner = createMigrationRunner(adapter, migrations)
await runner.up()
await db.migrate(migrations)
```
The runner checksums each `up.sql` and detects drift if a previously applied migration changes. Use `runner.status()` to inspect applied/pending/drifted state, and `runner.down()` to revert.
The database checksums each `up.sql` and detects drift if a previously applied migration changes.
Use `db.migrationStatus(migrations)` to inspect applied/pending/drifted state, and
`db.migrate(migrations, { direction: 'down' })` to revert.
## Input Validation (`remix/data-schema`)
@@ -95,6 +95,7 @@ Client entry props must be serializable: strings, numbers, booleans, `null`, `un
Use `run` to start the client runtime. It scans the document for client entry markers, loads modules, and hydrates each one:
```tsx
import type { ResolveFrameOptions } from 'remix/ui'
import { run } from 'remix/ui'
const app = run({
@@ -102,14 +103,31 @@ const app = run({
let mod = await import(moduleUrl)
return mod[exportName]
},
async resolveFrame(src, signal, target) {
let headers = new Headers({ accept: 'text/html' })
if (target) headers.set('x-remix-target', target)
let response = await fetch(src, { headers, signal })
async resolveFrame(src, options) {
let headers = new Headers({ accept: 'text/html', 'x-remix-frame': 'true' })
if (options?.target) headers.set('x-remix-target', options.target)
let response = await fetch(src, {
body: getRequestBody(options),
headers,
method: options?.method,
signal: options?.signal,
})
return response.body ?? (await response.text())
},
})
function getRequestBody(options?: ResolveFrameOptions): BodyInit | undefined {
let formData = options?.formData
if (!formData) return
if (options.encType !== 'application/x-www-form-urlencoded') return formData
let body = new URLSearchParams()
for (let [name, value] of formData) {
body.append(name, typeof value === 'string' ? value : value.name)
}
return body
}
app.addEventListener('error', (event) => {
console.error('Component error:', event.error)
})
@@ -120,7 +138,7 @@ await app.ready()
### `run` options
- **`loadModule(moduleUrl, exportName)`** (required) — return the component function for each client entry. Typically uses dynamic `import()`.
- **`resolveFrame(src, signal, target)`** (optional) — called when a `<Frame>` loads or reloads content. `target` is available when frame targeting matters.
- **`resolveFrame(src, options)`** (optional) — called when a `<Frame>` loads or reloads content and for intercepted link and form navigations. `options` may contain `signal` and `target`; non-GET forms also provide `formData`, `method`, and `encType`.
### `app` methods
@@ -189,6 +207,20 @@ handle.frames.top.reload()
When a frame reloads, matching DOM nodes are updated in place. Client entries receive updated props while preserving their local component state.
### Form navigation
When `run({ resolveFrame })` is active, eligible same-origin forms progressively enhance into frame navigations. Native validation and the form's `submit` event still run first.
- Forms target `handle.frames.top` by default.
- `rmx-target` selects a named frame.
- `rmx-src` selects a different frame request URL while preserving the form action as the navigation destination.
- `rmx-history="push|replace"` overrides how the navigation updates history.
- `rmx-reset-scroll="false"` preserves scroll position.
- `rmx-document` opts back into a document submission.
- Cross-origin forms, `method="dialog"`, and `target="_blank"` remain browser-owned.
GET controls are already encoded in `src`, so GET forms reach the resolver like links. Non-GET forms provide their native `FormData`, effective method, and encoding. The resolver owns body encoding and method-override conventions. Non-GET submissions to the current URL replace its history entry; GET submissions and submissions to a different URL push one. The `rmx-history` attribute overrides those defaults.
### Nested frames
Frames can nest. Each frame owns its own DOM region and hydrates client entries independently. During SSR, `handle.frame.src` points at the frame being rendered, while `handle.frames.top.src` stays fixed at the outer document URL.
@@ -252,7 +284,7 @@ navigate('/dashboard', { history: 'replace' })
Options: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
Attributes understood by the runtime: `rmx-target`, `rmx-src`, `rmx-document`.
Attributes understood by the runtime: `rmx-target`, `rmx-src`, `rmx-history`, `rmx-reset-scroll`, `rmx-document`.
## Head Management
@@ -65,8 +65,9 @@ let router = createRouter({ middleware })
### Static files vs browser modules
- Use `staticFiles()` for files that should be served directly from disk, such as images, fonts, or already-built assets in `public/`
- Use `staticFiles()` for files that should be served directly from disk, such as images, fonts, or already-built assets in the root `public/` directory
- Use `remix/assets` when browser modules should be compiled and served from source files with import rewriting, preloads, or fingerprinted URLs
- `public/` directories inside `app/` hold browser-reachable source for the asset server
### Ordering notes
@@ -118,11 +119,11 @@ Use `context.set(key, value)` to add typed values accessible downstream via `con
```typescript
import type { Middleware } from 'remix/router'
import { Database } from 'remix/data-table'
import { databaseContext } from '~/middleware/database.ts'
export function loadDatabase(): Middleware {
return async (context, next) => {
context.set(Database, db)
context.set(databaseContext, db)
return next()
}
}
@@ -152,11 +153,11 @@ export function requireAdmin(): Middleware {
// app/utils/context.ts
import { getContext } from 'remix/middleware/async-context'
import { Auth } from 'remix/middleware/auth'
import { Database } from 'remix/data-table'
import { databaseContext } from '~/middleware/database.ts'
import { Session } from 'remix/session'
export function getCurrentDb() {
return getContext().get(Database)
return getContext().get(databaseContext)
}
export function getCurrentSession() {
@@ -115,6 +115,8 @@ Adds client-side navigation behavior to any element. Makes non-anchor elements b
Options match `NavigationOptions`: `src`, `target`, `history` (`'push' | 'replace'`), `resetScroll`.
On a native anchor, the `history` option renders the corresponding `rmx-history="push|replace"` attribute so the enhanced navigation uses the same history behavior.
## Native press and keyboard interactions
Use native DOM events directly with `on(...)`. For buttons and links, `click` already includes keyboard activation when the element has the right semantics:
@@ -103,7 +103,7 @@ export const search = createAction(routes.search, {
The handler receives a context object with:
- `get(key)` — read a value set by middleware (e.g. `get(Database)`, `get(Session)`, `get(Auth)`)
- `get(key)` — read a value set by middleware (e.g. `get(databaseContext)`, `get(Session)`, `get(Auth)`)
- `params` — typed route params
- `url` — the request URL
- `request` — the raw `Request`
@@ -132,7 +132,7 @@ For pages, render a component tree and return the resulting `Response`:
```typescript
async handler({ get }) {
let db = get(Database)
let db = get(databaseContext)
let books = await db.findMany(books, { orderBy: ['id', 'asc'] })
return render(<IndexPage books={books} />)
}
@@ -152,7 +152,7 @@ async create({ get }) {
return render(<NewBookPage errors={parsed.issues} />, { status: 400 })
}
let db = get(Database)
let db = get(databaseContext)
let book = await db.create(books, parsed.value)
return redirect(routes.books.show.href({ slug: book.slug }), 303)
@@ -167,7 +167,7 @@ For expected failures — validation, conflict, not found — return a `Response
```typescript
async show({ get, params }) {
let db = get(Database)
let db = get(databaseContext)
let book = await db.find(books, params.bookId)
if (!book) return new Response('Not Found', { status: 404 })
return render(<ShowPage book={book} />)
@@ -208,7 +208,7 @@ If you find yourself returning JSON for what is really a browser form submission
A controller owns the direct leaf routes in one route map. Each key in `actions` matches a direct leaf route key in the route definition passed to `router.map(...)`. Nested route-map keys do not belong inside a controller's `actions`; map those route maps with their own controllers.
Configure `RouterTypes.context` with your app context in the router module, then use `createController()` so `get(Database)`, `get(Session)`, `get(Auth)`, etc. are typed against your middleware stack without repeating a type clause on every controller.
Configure `RouterTypes.context` with your app context in the router module, then use `createController()` so `get(databaseContext)`, `get(Session)`, `get(Auth)`, etc. are typed against your middleware stack without repeating a type clause on every controller.
```typescript
import { createController } from 'remix/router'
@@ -218,13 +218,13 @@ import { routes } from '../routes.ts'
export default createController(routes.books, {
actions: {
async index({ get }) {
let db = get(Database)
let db = get(databaseContext)
let items = await db.findMany(books, { orderBy: ['id', 'asc'] })
return render(<IndexPage items={items} />)
},
async show({ get, params }) {
let db = get(Database)
let db = get(databaseContext)
let book = await db.find(books, params.bookId)
if (!book) return new Response('Not Found', { status: 404 })
return render(<ShowPage book={book} />)
@@ -359,4 +359,4 @@ declare module 'remix/router' {
}
```
This gives typed `context.get(Database)`, `context.get(Session)`, `context.get(Auth)`, etc.
This gives typed `context.get(databaseContext)`, `context.get(Session)`, `context.get(Auth)`, etc.
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: rust-testing
description: Rust testing patterns including unit tests, integration tests, async testing, property-based testing, mocking, and coverage. Follows TDD methodology.
description: Rust testing patterns including unit tests, integration tests, async testing, property-based testing, mocking, and coverage. Follows TDD methodology. Use when writing Rust tests — unit, integration, async, property-based, or coverage.
metadata:
origin: ECC
---
+3 -4
View File
@@ -25,13 +25,12 @@ fi
# Flyline — enhanced Bash line editing (machine-optional, guarded)
[ -f "$HOME/.local/lib/libflyline.so" ] && enable -f "$HOME/.local/lib/libflyline.so" flyline
# atuin shell history (needs bash-preexec)
[ -f "$HOME/.atuin/bin/env" ] && . "$HOME/.atuin/bin/env"
[ -f "$HOME/.bash-preexec.sh" ] && . "$HOME/.bash-preexec.sh"
command -v atuin >/dev/null 2>&1 && eval "$(atuin init bash)"
# Generated for envman. Do not edit.
[ -s "$HOME/.config/envman/load.sh" ] && . "$HOME/.config/envman/load.sh"
# Prompt — starship, consistent with fish
command -v starship >/dev/null 2>&1 && eval "$(starship init bash)"
. "$HOME/.atuin/bin/env"
eval "$(atuin init bash)"
+5 -5
View File
@@ -262,13 +262,13 @@
"matcher": ".*"
},
{
"matcher": "tldraw-offline",
"hooks": [
{
"type": "command",
"command": "sh '/home/prad/skills/tldraw-offline/inject-server-context.sh' SubagentStart"
"command": "sh '/home/prad/skills/tldraw-offline/inject-server-context.sh' SubagentStart",
"type": "command"
}
]
],
"matcher": "tldraw-offline"
}
],
"SubagentStop": [
@@ -310,4 +310,4 @@
"skipAutoPermissionPrompt": true,
"skipDangerousModePermissionPrompt": true,
"tui": "fullscreen"
}
}
-1
View File
@@ -7,7 +7,6 @@ fish_add_path $HOME/.grok/bin
fish_add_path $HOME/.opencode/bin
fish_add_path -a $HOME/.foundry/bin
fish_add_path -a $HOME/local/share/pnpm
source $HOME/.atuin/bin/env.fish
# pnpm
set -gx PNPM_HOME $HOME/.local/share/pnpm
-2
View File
@@ -1,2 +0,0 @@
source "$HOME/.atuin/bin/env.fish"
+2 -3
View File
@@ -30,10 +30,9 @@ status is-interactive; and test "$FISH_BOTTOM_PROMPT" = 1; and tput cup $LINES 0
fish_add_path $HOME/.grok/bin
# <<< grok installer <<<
# Generated for envman. Do not edit.
test -s ~/.config/envman/load.fish; and source ~/.config/envman/load.fish
if status is-interactive
atuin init fish | source
end
# Generated for envman. Do not edit.
test -s ~/.config/envman/load.fish; and source ~/.config/envman/load.fish
@@ -1,25 +1,33 @@
function fish_user_key_bindings
# Keys owned by kitty.conf never reach fish and are NOT bound here:
# ctrl-g → lazygit overlay, ctrl-a → omp hsplit, ctrl-y → yazi vsplit,
# ctrl-h/j/k/l → vim-aware split nav (pass_keys.py).
# ctrl-e: yazi, same as ctrl-f (cd's to yazi's exit dir on quit).
bind ctrl-e 'yy; commandline -f repaint'
bind ctrl-b purple repaint
bind ctrl-y clip-last
# Vi mode first: it rebinds the preset tables and (on first activation)
# re-triggers this function via the fish_key_bindings handler, so custom
# binds must come after it to land on top.
fish_vi_key_bindings
bind ctrl-o 'gho; commandline -f repaint'
bind ctrl-p 'pj; commandline -f repaint'
bind ctrl-a 'omp; commandline -f repaint'
# bind \ca 'launch_omp_livediff_overlay; commandline -f repaint'
# Vi mode types in `insert`; bind customs there AND in `default` (normal)
# so the keys work regardless of mode.
for mode in insert default
# Keys owned by kitty.conf never reach fish and are NOT bound here:
# ctrl-g → lazygit overlay, ctrl-a → omp hsplit, ctrl-y → yazi vsplit,
# ctrl-h/j/k/l → vim-aware split nav (pass_keys.py).
# ctrl-e: yazi, same as ctrl-f (cd's to yazi's exit dir on quit).
bind -M $mode ctrl-e 'yy; commandline -f repaint'
bind -M $mode ctrl-b purple repaint
bind -M $mode ctrl-y clip-last
# ctrl-f: pick file(s) with television and open in nvim.
bind ctrl-f 'yy; commandline -f repaint'
bind ctrl-g 'lazygit; commandline -f repaint'
bind -M $mode ctrl-o 'gho; commandline -f repaint'
bind -M $mode ctrl-p 'pj; commandline -f repaint'
bind -M $mode ctrl-a 'omp; commandline -f repaint'
# ctrl-c routes through the clear wrapper (kitty scrollback + prompt
# pinned to the bottom) AND wipes any text typed on the command line.
bind ctrl-c 'commandline -r ""; clear; commandline -f repaint'
# ctrl-f: pick file(s) with television and open in nvim.
bind -M $mode ctrl-f 'yy; commandline -f repaint'
bind -M $mode ctrl-g 'lazygit; commandline -f repaint'
# ctrl-q mirrors ctrl-d's preset (`exit`) — quit the shell.
bind ctrl-q exit
# ctrl-c routes through the clear wrapper (kitty scrollback + prompt
# pinned to the bottom) AND wipes any text typed on the command line.
bind -M $mode ctrl-c 'commandline -r ""; clear; commandline -f repaint'
# ctrl-q mirrors ctrl-d's preset (`exit`) — quit the shell.
bind -M $mode ctrl-q exit
end
end
+1 -2
View File
@@ -46,8 +46,7 @@ map ctrl+shift+g launch_ov lazygit
map ctrl+shift+f toggle_layout stack
map ctrl+shift+e launch_tb nvim
map ctrl+shift+a launch_vs omp
# suvadu history picker: pick → execute → drop to shell (script: ~/.local/bin/suv-pick)
map ctrl+shift+p launch_hs suv-pick
map ctrl+shift+p launch_hs atuin-pick.py
# Misc
map ctrl+shift+c copy_to_clipboard
@@ -102,7 +102,7 @@ window-rule {
match app-id="todoist"
match app-id="com.anthropic.Claude"
match app-id="Hermes"
match app-id="chromium"
open-on-output "eDP-1"
}
@@ -8,7 +8,7 @@ output "Tianma Microelectronics Ltd. TL134ADXP03 Unknown" {
always-center-single-column true
center-focused-column "never"
gaps 8
default-column-width { proportion 0.9; }
default-column-width { proportion 1.0; }
preset-column-widths {
proportion 0.5
proportion 1.0
+2 -2
View File
@@ -1,6 +1,6 @@
[[plugin.deps]]
use = "yazi-rs/plugins:smart-filter"
rev = "0be29a9"
rev = "3f2b882"
hash = "d6f7351ea25a43c9b5f4315fa1e23935"
[[plugin.deps]]
@@ -10,7 +10,7 @@ hash = "8c1b36103121a66b37308afaf7124575"
[[plugin.deps]]
use = "yazi-rs/plugins:smart-enter"
rev = "0be29a9"
rev = "3f2b882"
hash = "187cc58ba7ac3befd49c342129e6f1b6"
[[plugin.deps]]
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""atuin-pick: atuin history picker for a kitty split (ctrl+shift+p).
atuin search -i draws its TUI on the tty and prints the selected command
to stdout (Esc prints nothing). Filter to history from the current
directory. Run the selection with rc loaded so aliases and functions
resolve, then drop to an interactive shell.
"""
import os
import subprocess
import sys
# kitty launches this outside a shell, so atuin's init never ran.
if "ATUIN_SESSION" not in os.environ:
os.environ["ATUIN_SESSION"] = subprocess.run(
["atuin", "uuid"], stdout=subprocess.PIPE, text=True
).stdout.strip()
sel = subprocess.run(
["atuin", "search", "-i", "--filter-mode", "directory"],
stdout=subprocess.PIPE,
text=True,
).stdout.strip()
if sel:
print(f"\u276f {sel}", file=sys.stderr)
subprocess.run(["zsh", "-ic", "--", sel])
os.execvp("zsh", ["zsh", "-i"])
-11
View File
@@ -1,11 +0,0 @@
#!/bin/zsh
# suv-pick: suvadu history picker for a kitty split (ctrl+shift+p).
# suv search draws its TUI on stderr and prints the selected command to
# stdout (Esc prints nothing). Run the selection with rc loaded so aliases
# and functions resolve, then drop to an interactive shell.
sel="$(command suv search)"
if [[ -n "$sel" ]]; then
print -ru2 -- "❯ $sel"
zsh -ic -- "$sel"
fi
exec zsh -i
+5 -2
View File
@@ -29,8 +29,7 @@ fi
# ZLE keybinds — zsh ports of fish_user_key_bindings.fish
[ -f "$HOME/.config/zsh/keybinds.zsh" ] && . "$HOME/.config/zsh/keybinds.zsh"
# suvadu shell history (replaces atuin here; fish keeps atuin — suvadu has no
# fish support yet, bash still on atuin too)
# suvadu shell history
command -v suv >/dev/null 2>&1 && eval "$(suv init zsh)"
# Generated for envman. Do not edit.
@@ -49,3 +48,7 @@ for _zsh_hl in \
[ -r "$_zsh_hl" ] && . "$_zsh_hl" && break
done
unset _zsh_hl
. "$HOME/.atuin/bin/env"
eval "$(atuin init zsh)"