--- name: new-deno-app description: Scaffold a new Deno 2 + Hono + Deno KV + Eta + HTMX app with password auth and PWA support. Use when the user wants to create a new personal web app following this stack. disable-model-invocation: true --- You are scaffolding a new personal web app. These apps follow a strict set of conventions: - Deno 2 runtime, no build step — TypeScript runs natively - Hono for routing (from JSR: `jsr:@hono/hono@^4`) - Deno KV for the database (built-in to Deno) - Eta for server-side HTML templates (from JSR: `jsr:@eta-dev/eta@^3`) - HTMX 2 for frontend interactivity (CDN, no npm) - PicoCSS classless for styling (CDN, no npm) - Single shared `APP_PASSWORD` env var for auth — SHA-256 hash in a session cookie - PWA: `manifest.json` + minimal pass-through service worker - Deployed to Deno Deploy via GitHub push to `main` ## Step 1: Gather information Always ask the user for all of the following interactively — do not attempt to parse `$ARGUMENTS`: 1. **app-name** — kebab-case, used for directory name and GitHub repo name 2. **Display Name** — full title shown in browser tab and manifest (e.g. "Book Tracker") 3. **Short name** — ≤12 characters, shown on Android home screen under the icon (e.g. "Books") 4. **Description** — one sentence describing what the app does 5. **Theme color** — hex color for PWA theme/background (default: `#ffffff`) 6. **Emoji** — single emoji used as the app logo in the header (default: `📋`) 7. **Data model** — what are the main things this app stores? Describe the entity name(s) and their fields briefly. Confirm all values with the user before proceeding. ## Step 2: Determine project directory Check whether the current working directory is empty (contains no files or subdirectories, ignoring dotfiles like `.git`). - **If the current directory is empty:** use it as the project root. All files will be created here. - **If the current directory is not empty:** tell the user: "The current directory isn't empty. Please create and switch to a new empty directory for the project, then run `/new-deno-app` again." Stop here — do not proceed. ## Step 3: Create project Create all files in the project root determined in Step 2. All files below use the exact patterns from the reference app. ### `deno.json` ```json { "imports": { "hono": "jsr:@hono/hono@^4", "hono/cookie": "jsr:@hono/hono@^4/cookie", "hono/deno": "jsr:@hono/hono@^4/deno", "@eta-dev/eta": "jsr:@eta-dev/eta@^3", "@std/assert": "jsr:@std/assert@^1" }, "tasks": { "dev": "deno run --watch --allow-net --allow-env --allow-read --unstable-kv main.ts", "start": "deno run --allow-net --allow-env --allow-read --unstable-kv main.ts", "test": "DENO_TLS_CA_STORE=system deno test --allow-env --unstable-kv", "fmt": "deno fmt", "lint": "deno lint", "check": "deno fmt --check && deno lint && deno check main.ts src/**/*.ts" }, "fmt": { "lineWidth": 100, "semiColons": false, "singleQuote": false }, "lint": { "rules": { "exclude": ["no-explicit-any"] } } } ``` ### `src/auth.ts` This is the exact auth pattern. Copy it verbatim — only the dev default password can be changed. ```typescript import type { Context, Next } from "hono" import { getCookie, setCookie } from "hono/cookie" async function hashPassword(password: string): Promise { const data = new TextEncoder().encode(password) const buf = await crypto.subtle.digest("SHA-256", data) return Array.from(new Uint8Array(buf)) .map((b) => b.toString(16).padStart(2, "0")) .join("") } export function getSessionToken(): Promise { return hashPassword(Deno.env.get("APP_PASSWORD") ?? "changeme") } export function verifyPassword(submitted: string): boolean { const expected = Deno.env.get("APP_PASSWORD") ?? "changeme" return submitted === expected } export async function setSessionCookie(c: Context): Promise { const token = await getSessionToken() const isProd = Deno.env.get("DENO_DEPLOYMENT_ID") !== undefined setCookie(c, "session", token, { httpOnly: true, sameSite: "Strict", secure: isProd, path: "/", maxAge: 60 * 60 * 24 * 30, }) } export async function authMiddleware(c: Context, next: Next) { if (c.req.path === "/login") { return next() } const session = getCookie(c, "session") const expected = await getSessionToken() if (session !== expected) { if (c.req.header("HX-Request")) { c.header("HX-Redirect", "/login") return c.text("", 401) } return c.redirect("/login") } return next() } ``` ### `src/db.ts` Generate this based on the user's described data model. The KV singleton **must** store a `Promise` (not the resolved instance) to prevent races when concurrent calls arrive before the first open resolves. Always export `closeKv()` for tests. Template — replace `Item` / `["items", id]` with the actual entities: ```typescript import type { Item } from "./types.ts" let _kv: Promise | null = null function kv(): Promise { if (!_kv) _kv = Deno.openKv() return _kv } export async function getItem(id: string): Promise { const db = await kv() return (await db.get(["items", id])).value } export async function listItems(): Promise { const db = await kv() const items: Item[] = [] for await (const entry of db.list({ prefix: ["items"] })) { items.push(entry.value) } return items.sort((a, b) => a.name.localeCompare(b.name)) } export async function saveItem(item: Item): Promise { const db = await kv() await db.set(["items", item.id], item) } export async function deleteItem(id: string): Promise { const db = await kv() await db.delete(["items", id]) } export function closeKv(): void { _kv?.then((db) => db.close()) _kv = null } ``` ### `src/types.ts` Define TypeScript interfaces based on the user's data model. Every entity must have: - `id: string` — use `crypto.randomUUID()` when creating - `createdAt: string` — ISO date string ### `src/views/eta.ts` ```typescript import { Eta } from "@eta-dev/eta" const isProd = Deno.env.get("DENO_DEPLOYMENT_ID") !== undefined export const deploymentId = Deno.env.get("DENO_DEPLOY_BUILD_ID") ?? "dev" // Resolve template directory relative to this file so it works on Deno Deploy const templatesDir = new URL("../templates", import.meta.url).pathname export const eta = new Eta({ views: templatesDir, cache: isProd, }) ``` ### `src/templates/layout.eta` Fill in Display Name, emoji, short_name, and theme color. Keep all PWA meta tags. ```html <%= it.title %> – <Display Name>
<%~ it.body %>
Build: <%= it.deploymentId %>
``` If the app has multiple sections, add nav links inside `
    ` in the `