@kentcdodds/package-app-kit
Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.
starter-remix/AGENTS.template.md
148 lines · 8.7 KB · MarkdownPACKAGE_NAME — agent notes
Scaffolded from @kentcdodds/package-app-kit/starter-remix. Human Intent lives in README.
- Handoff: open private hosted apps with Open Package App from kody.codes; re-open from the package page when you need a fresh session or service worker.
After scaffolding
After create-package-app lands this tree (or you copy starter-remix/), work through this checklist before treating the app as product-ready:
- Brand the list mark — Generate a distinctive square mark for this app and place it at
.kody/icon.png(do not reuse the kit demo race-driver mark) (also.svg/.webp/.jpg/.jpeg). Prefer PNG on the Artifacts git lane. This is package / repo / community list identity. The scaffold ships.kody/icon.svgas a starting point — replace it with the product mark. - Brand PWA + in-app chrome marks — Place matching product install icons at
icons/icon-192.pngandicons/icon-512.png(add maskable variants when the manifest uses them). Serve them from app routes (iconPngResponseorimage/pnghandlers) and point favicon / header<img>atroutes.…href(). For a hero or alternate bust, add the same kind of route (for example/brand/logo-full.png). Keep list identity in.kody/; keeppublic/for CSS,sw.js, and other asset-host files. - Fill product identity — Set
__APP_TITLE__/ title, description, andpackageName(and related placeholders) to the real product names. - Keep local version writers — Use this package's
src/version.ts+src/record-version.tsagainst this app'spackageStorage. Call this package's./record-version(kody:__PACKAGE_NAME__/record-version), not the kit's export. - Record each publish — After every successful publish, call this package's
./record-versionwith the published SHA (and message /committedAtwhen available). - Remix recipe (#2312) — Confirm root
tsconfig.jsonhas"jsx": "react-jsx"+"jsxImportSource": "remix/ui"; put both file pragmas at the top of every SSR.tsxthat uses JSX (/** @jsxRuntime automatic */and/** @jsxImportSource remix/ui */);app/router.tsremounts the Request andexport default { fetch }; islands useclientEntry('kody:app#…', …)with matching browser registry keys. - Match manifest colors to the mark — Set
themeColor/backgroundColorinbuildWebManifest(seeapp/controllers/manifest.ts) and the documenttheme-colormeta so they harmonize with the mark. - Optional UI icons — Add Lucide glyphs via
node scripts/add-lucide-icon.mjs <names>intoapp/icons/when the UI needs icon-only or labeled controls.
Then smoke with packageAppFetch (paths in Smoke below).
Remix recipe (framework-agnostic host)
"kody": {
"app": {
"entry": "./app/router.ts",
"client": "./app/assets/entry.ts",
"assets": "./public"
}
}- tsconfig — root
tsconfig.jsonwith"jsx": "react-jsx"and"jsxImportSource": "remix/ui"(supporting config). - Per-file JSX pragmas (required) — at the top of every SSR
.tsxthat uses JSX:/** @jsxRuntime automatic */ /** @jsxImportSource remix/ui */ - Router
app/router.ts— remount the Request when routes includeappBasePath, thenexport default { fetch(request) { return router.fetch(remountRequest(request)) } }. Keep the Worker/server graph free ofapp/assets/entry.tsexcept shared island modules that stay free ofkody:. - Routes
app/routes.ts—route(packageContext?.appBasePath ?? '', { … })sohref()/ redirects stay mounted. - Client
app/assets/entry.ts—run({ loadModule })registry of named islands. - Islands —
clientEntry('kody:app#Name', function Name…)+ matching registry key. - Assets
public/—styles.css,sw.js(reads__version.json; no content hash in source).
Imports — pin platform remix/* only
import { createRouter } from 'remix/router'
import { form, route } from 'remix/routes'
import { formData } from 'remix/middleware/form-data'
// In actions: context.get(FormData) — FormData is the global Web API constructor (context key).
// Do not import FormData from remix/middleware/form-data (it is not exported). context.formData is equivalent but docs use get().
import { redirect } from 'remix/response/redirect'
import { renderToStream } from 'remix/ui/server'
import { clientEntry, on, run } from 'remix/ui'
import { KodyRuntime, packageContext } from 'kody:runtime'Forbid:
@remix-run/*independencies/ import maps- Fat npm
remixruntime dependency (optionaldevDependencies.remix@3.0.0-rc.2for editor types only) - Vite / HMR / Pitlane tooling inside the Worker publish
Kit (Worker / SSR only)
import { formatAboutPageData } from 'kody:@kentcdodds/package-app-kit/about'
import { buildWebManifest } from 'kody:@kentcdodds/package-app-kit/sw'
import { defaultManifestIcons, iconPngResponse } from 'kody:@kentcdodds/package-app-kit/icons'Version metadata: use this package's local ./record-version (src/record-version.ts → this app's packageStorage). Do not re-export or call kody:@kentcdodds/package-app-kit/record-version from execute — that writes the kit's storage and fails provenance. About / /api/version import readRecordedVersion relatively from ../../src/record-version.ts.
Browser islands must not import kody: / kody:@….
List / identity mark
Put the package list mark at .kody/icon.png (also .svg / .webp / .jpg / .jpeg). This scaffold includes .kody/icon.svg — replace it with your product's official logo (prefer PNG on the git lane). Do not add root community-icon.*. Do not move PWA icons (icons/icon-192.png, routes served by kit iconPngResponse) into .kody/.
PWA / SW
Register ${assetBasePath}/sw.js with { scope: ${appBasePath}/ }. Precache via GET …/_assets/__version.json → clientModuleUrl. Icons via kit iconPngResponse routes (192 + 512). Manifest crossorigin="use-credentials".
Optional L3 — Realtime
Package apps can add live sync with kit ./realtime + ./realtime-client (see kit AGENTS L3 — Realtime). Typical recipe: export handleRealtimeEvent from app/router.ts, broadcast after durable writes via KodyRuntime.realtime, connect from a browser island with connectPackageRealtime. Do not wire WebSocket into every scaffold by default — adopt when the product needs multi-session notify (Notes-style).
Smoke
import { kody } from 'kody:runtime'
export default async function main() {
const paths = ['/', '/notes', '/about', '/manifest.webmanifest', '/icons/icon-192.png', '/icons/icon-512.png', '/health']
const out = []
for (const path of paths) {
const res = await kody.packageAppFetch({ kody_id: '__PACKAGE_ID__', path })
out.push({ path, status: res.status, type: res.headers?.['content-type'] || res.headers?.['Content-Type'] })
}
return out
}Prefer zero content layout shift (CLS)
Keep document flow stable. Update-available, toasts, and other post-hydration chrome use fixed overlays (see UpdateBanner / .pak-update-toast). Do not inject in-flow banners that push content down. Reserve height for async UI; hide install chrome in display-mode: standalone.
Tooltips (data-tip)
Tip when the tip adds information: icon-only install in the header, truncated SHA, ISO behind local time. Labeled nav and action buttons rely on their visible words. Color swatches use visible text labels.
Install header icon
Place InstallCta in the header brand row to the right of the title. Keep a reserved same-size slot (pak-install-slot) so BIP reveal does not shift layout. No large in-flow Install button.
Optimistic UI + press feedback
Immediate data-pending / aria-busy on press. Use app/ui/busy.ts (createBusyGate) for spin-delay timing: delay busy labels ~200ms, keep them ≥400ms once shown, so fast ops do not flash "Loading…".
Lucide icons
# From the kit repo while editing the starter twin, or copy script into the app:
node scripts/add-lucide-icon.mjs download trash-2 plusImport generated Remix Handle components from app/icons/{name}.tsx (they return a render function). Icon-only controls get data-tip + aria-label; labeled buttons keep visible text.
packageStorage migrations
import {
createMigrationRunner,
} from 'kody:@kentcdodds/package-app-kit/storage-migrations'
const ensureSchema = createMigrationRunner({
storage: context.get(KodyRuntime).packageStorage(),
versionKey: '__PACKAGE_ID__-schema-version',
migrations: [/* { version, name, up } */],
})
await ensureSchema()See app/data/notes.ts for a copy-paste example.