Skip to content
← Public packages

@kentcdodds/package-app-kit

Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.

FollowFork

starter-tanstack/AGENTS.template.md

182 lines · 11.6 KB · Markdown

PACKAGE_NAME — agent notes

Scaffolded from @kentcdodds/package-app-kit/starter-tanstack. 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-tanstack lands this tree (or you copy starter-tanstack/), work through this checklist before treating the app as product-ready:

  1. List mark (TanStack default included) — This scaffold ships the square TanStack koala mark at .kody/icon.png / .kody/icon.svg (SVG wraps the PNG for text-only packageSave). Keep it unless the product needs its own brand — then replace with a distinctive square mark (do not reuse the kit demo race-driver). Prefer PNG on the Artifacts git lane. This is package / repo / community list identity.
  2. PWA + in-app chrome marks (TanStack default included) — Favicon / header / PWA icons already default to the same TanStack koala via src/icon-png-bytes.ts and /icons/icon-192.png + /icons/icon-512.png routes (git lane also has matching icons/*.png). Replace those bytes/files only when product-branding. Serve from app routes 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. TanStack recipe (#2312) — Confirm root tsconfig.json has "jsx": "react-jsx" + "jsxImportSource": "react"; app/router.ts remounts the Request and export default { fetch }; app/routeTree.tsx builds @tanstack/react-router routes; SSR uses react-dom/server.edge renderToReadableStream in app/ssr.tsx (no TanStack Start / Vite); client app/assets/entry.ts calls hydrateRoot + RouterProvider.
  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).

Kody CLI workflow

This package ships npm scripts around the Kody CLI (@kodycodes/cli, pinned in devDependencies, so run npm install once). Each script is a thin wrapper; the raw command is listed so agents without npm (or with only MCP) can follow along.

StepScriptRaw command
Sign in + git access (humans)npm run setupkody login && kody auth setup-git
Pull—git clone https://kody.codes/__PACKAGE_NAME__.git then git pull
Run locally (mock)npm run devkody dev --target mock
Run against real datanpm run dev:prodkody dev --target prod
Fetch one pathnpm run fetch -- /healthkody dev fetch /health
Run a job oncenpm run job -- <name>kody dev job <name>
Send a webhooknpm run webhook -- <name> --body fixtures/webhook.example.jsonkody dev webhook <name> --body <file>
Emit an eventnpm run emit -- <event> --body fixtures/event.example.jsonkody dev emit <event> --body <file>
Bundle + publish checksnpm run checkkody dev check
Typesnpm run typechecktsc --noEmit
Testsnpm testnode --test (smoke tests call kody dev fetch on mock)
Push—git push
Publishnpm run publish:kody (add -- <package_id> if your CLI rejects the name)tests, node scripts/prepublish-check.mjs, then kody package publish <package_id or __PACKAGE_NAME__>

Loop: pull, edit, npm run dev (or npm run fetch -- <path>), npm test, commit, git push, npm run publish:kody. Add --dry-run to job / webhook / emit to see what would run without side effects. Mock is the default target: scratch storage and fake credentials, so unpublished code never touches real data. Use dev:prod only when you mean to.

Signing in (explicit token lifetimes)
  • Humans: npm run setup (browser kody login, then kody auth setup-git, which defaults to --lifetime long).
  • Agents on a task: get a one-shot kody_bc_… code from MCP cliCredentialBootstrap, then npx kody auth bootstrap --code <code> --lifetime short (1h idle / 24h max). Only use --lifetime long (14d idle / 3mo max) for a long-running agent the owner set up on purpose. Then npx kody auth setup-git --lifetime short for git access.
  • Always pass --lifetime explicitly. Never print, log, commit, or paste tokens. Do not call kody api cliCredentialBootstrapRedeem (it prints the token). kody status / kody whoami confirm sign-in without showing it.
  • Package code reads Kody credentials through the runtime, never from files in this repo. scripts/prepublish-check.mjs refuses to publish if a tracked file looks like it contains one.
Without a shell (MCP-only agents)

Scripts are optional. The same loop works through Kody MCP tools: edit files with packageSave (or repo sessions), publish with packagePublishExternalPush, and smoke with packageAppFetch (see Smoke below). Before publishing, re-read After scaffolding and confirm README/AGENTS still describe the app.

TanStack 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": "react".
  • Router app/router.ts — remount the Request for appBasePath, then export default { fetch(request) { … } } dispatching API routes + renderApp SSR.
  • Route tree app/routeTree.tsx — @tanstack/react-router createRootRouteWithContext + page routes; basepath matches the mount.
  • SSR app/ssr.tsx — createMemoryHistory + renderToReadableStream(<RouterProvider />) (Workers-safe; not TanStack Start).
  • Routes helpers app/routes.ts — createRoutes(appBasePath) for hrefs (no kody:runtime in this module).
  • Client app/assets/entry.ts — hydrateRoot(document, <RouterProvider />) + SW register.
  • Assets public/ — styles.css, sw.js (reads __version.json; no content hash in source).

Imports — React + TanStack Router (no Vite / Start)

import {
  createRouter,
  createRootRouteWithContext,
  createRoute,
  createMemoryHistory,
  createBrowserHistory,
  RouterProvider,
  Link,
} from '@tanstack/react-router'
import { renderToReadableStream } from 'react-dom/server.edge'
import { hydrateRoot } from 'react-dom/client'
import { packageContext, packageStorage } from 'kody:runtime'

Forbid:

  • TanStack Start / Vite / Pitlane tooling inside the Worker publish
  • remix/component JSX pragmas (this starter is standard React)
  • Importing kody: / kody:@… from the browser client graph

Kit (Worker / SSR only)

import { formatAboutPageData } from 'kody:@kentcdodds/package-app-kit/about'
import { buildWebManifest } from 'kody:@kentcdodds/package-app-kit/sw'
import { starterManifestIcons, iconPngResponse } from '../src/icon-png-bytes.ts'

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 the TanStack koala mark (.kody/icon.png + .kody/icon.svg base64 wrapper) — keep it unless replacing with the 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 src/icon-png-bytes.ts) into .kody/.

PWA / SW

Register ${assetBasePath}/sw.js with { scope: ${appBasePath}/ }. Precache via GET …/_assets/__version.json → clientModuleUrl. Icons via starter src/icon-png-bytes.ts routes (192 + 512; TanStack koala default). 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 React SVG components from app/icons/{name}.tsx. Icon-only controls get data-tip + aria-label; labeled buttons keep visible text.

packageStorage migrations

import { packageStorage } from 'kody:runtime'
import {
  createMigrationRunner,
} from 'kody:@kentcdodds/package-app-kit/storage-migrations'

const ensureSchema = createMigrationRunner({
  storage: packageStorage(),
  versionKey: '__PACKAGE_ID__-schema-version',
  migrations: [/* { version, name, up } */],
})
await ensureSchema()

See app/data/notes.ts for a copy-paste example.