Skip to content

Kody is live

Watch the launch video — what Kody is, and why it exists.

← Public packages

@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 · Markdown

PACKAGE_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:

  1. 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.svg as a starting point — replace it with the product mark.
  2. Brand PWA + in-app chrome marks — Place matching product install icons at icons/icon-192.png and icons/icon-512.png (add maskable variants when the manifest uses them). Serve them from app routes (iconPngResponse or image/png handlers) and point favicon / header <img> at routes.…href(). For a hero or alternate bust, add the same kind of route (for example /brand/logo-full.png). Keep list identity in .kody/; keep public/ for CSS, sw.js, and other asset-host files.
  3. Fill product identity — Set __APP_TITLE__ / title, description, and packageName (and related placeholders) to the real product names.
  4. Keep local version writers — Use this package's src/version.ts + src/record-version.ts against this app's packageStorage. Call this package's ./record-version (kody:__PACKAGE_NAME__/record-version), not the kit's export.
  5. Record each publish — After every successful publish, call this package's ./record-version with the published SHA (and message / committedAt when available).
  6. Remix recipe (#2312) — Confirm root tsconfig.json has "jsx": "react-jsx" + "jsxImportSource": "remix/ui"; put both file pragmas at the top of every SSR .tsx that uses JSX (/** @jsxRuntime automatic */ and /** @jsxImportSource remix/ui */); app/router.ts remounts the Request and export default { fetch }; islands use clientEntry('kody:app#…', …) with matching browser registry keys.
  7. Match manifest colors to the mark — Set themeColor / backgroundColor in buildWebManifest (see app/controllers/manifest.ts) and the document theme-color meta so they harmonize with the mark.
  8. Optional UI icons — Add Lucide glyphs via node scripts/add-lucide-icon.mjs <names> into app/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.json with "jsx": "react-jsx" and "jsxImportSource": "remix/ui" (supporting config).
  • Per-file JSX pragmas (required) — at the top of every SSR .tsx that uses JSX:
    /** @jsxRuntime automatic */
    /** @jsxImportSource remix/ui */
  • Router app/router.ts — remount the Request when routes include appBasePath, then export default { fetch(request) { return router.fetch(remountRequest(request)) } }. Keep the Worker/server graph free of app/assets/entry.ts except shared island modules that stay free of kody:.
  • Routes app/routes.tsroute(packageContext?.appBasePath ?? '', { … }) so href() / redirects stay mounted.
  • Client app/assets/entry.tsrun({ loadModule }) registry of named islands.
  • IslandsclientEntry('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/* in dependencies / import maps
  • Fat npm remix runtime dependency (optional devDependencies.remix@3.0.0-rc.2 for 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.jsonclientModuleUrl. 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 plus

Import 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.