- commit
- f561598c4516471b874320bc610408c9e928bc3a
- parent
- b054684f4fd13794a9e0ac2354a4795b4f975739
- author
- Dami <dami.etoile@gmail.com>
| M | deno.json | 2 | +- |
| A | jsr.json | 23 | +++ |
| A | package/ | 21 | +++ |
| A | package/ | 225 | ++++++++++++++++++++++++++++++ |
| A | package/ | 147 | ++++++++++++++++++++ |
| A | package/ | 96 | +++++++++++++ |
| A | package/ | 41 | +++++ |
| A | package/ | 5 | + |
@@ -26,7 +26,7 @@
2626 "check:migrations": "deno run --allow-read=apps/reference/src/migrations --allow-run tools/check_migrations.ts",2727 "check:astro": "LOFI_SKIP_JAZZ_MANAGED=1 JAZZ_ADMIN_SECRET= BACKEND_SECRET= deno run -A npm:astro@7.0.9 check --root apps/reference",2828 "check": "deno fmt --check && deno lint && deno check tools/*.ts spikes/jazz2-baseline/*.ts spikes/jazz2-baseline/src/*.ts apps/reference/*.ts apps/reference/src/**/*.ts apps/reference/src/**/*.tsx && deno task check:astro && deno task check:env && deno task check:secrets && deno task check:migrations && deno task test",29- "test:unit": "deno test --allow-net=127.0.0.1 --allow-read --allow-write=. --allow-run tools apps/reference/src/_lofi apps/reference/tests",29+ "test:unit": "deno test --allow-net=127.0.0.1 --allow-read --allow-write=. --allow-run tools package apps/reference/src/_lofi apps/reference/tests",3030 "test": "deno task test:unit",3131 "test:browser:install": "deno run -A npm:playwright@1.61.1 install chromium",3232 "test:golden:checkout": "deno run -A tools/golden_path_checkout.ts",@@ -0,0 +1,23 @@
1+{2+ "$schema": "https://jsr.io/schema/config-file.v1.json",3+ "name": "@nzip/lofi",4+ "version": "0.1.0",5+ "exports": {6+ "./create": "./package/create.ts"7+ },8+ "publish": {9+ "include": [10+ "README.md",11+ "apps/reference/astro.config.ts",12+ "apps/reference/public",13+ "apps/reference/src",14+ "apps/reference/tests",15+ "apps/reference/tsconfig.json",16+ "package"17+ ],18+ "exclude": [19+ "package/**/*_test.ts",20+ "package/testdata"21+ ]22+ }23+}@@ -0,0 +1,21 @@
1+2+3+import { createProject } from "./create_core.ts";4+5+function usage(): never {6+ console.error("usage: deno run -A jsr:@nzip/lofi/create <name>");7+ Deno.exit(2);8+}9+10+if (Deno.args.length !== 1 || Deno.args[0].startsWith("-")) usage();11+12+try {13+ const result = await createProject({ cwd: Deno.cwd(), name: Deno.args[0] });14+ console.log(`Created ${result.displayPath}`);15+ console.log("");16+ console.log("Next:");17+ for (const command of result.nextCommands) console.log(` ${command}`);18+} catch (error) {19+ console.error(`error: ${error instanceof Error ? error.message : String(error)}`);20+ Deno.exit(1);21+}@@ -0,0 +1,225 @@
1+import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";2+import { STARTER_TEMPLATE } from "./starter_template.ts";3+import { LOFI_PACKAGE_PREFIX } from "./version.ts";4+5+export type CreateProjectOptions = {6+ cwd: string;7+ name: string;8+ packagePrefix?: string;9+};10+11+export type CreateProjectResult = {12+ destination: string;13+ displayPath: string;14+ nextCommands: readonly [string, string];15+};16+17+const generatedGitignore = `.env18+.env.*19+!.env.example20+21+node_modules/22+.astro/23+.vite/24+dist/25+coverage/26+playwright-report/27+test-results/28+.playwright-cli/29+.nzip-*/30+.DS_Store31+`;32+33+const generatedEnvironment =34+ `# Optional public Jazz cloud configuration. Leave both blank for local-only mode.35+JAZZ_APP_ID=36+JAZZ_SERVER_URL=37+38+# Server-only credentials. Never expose these through PUBLIC_ or client code.39+JAZZ_ADMIN_SECRET=40+BACKEND_SECRET=41+`;42+43+function generatedReadme(name: string): string {44+ return `# ${name}45+46+This local-first PWA was created with \`@nzip/lofi\`.47+48+\`\`\`sh49+deno task dev50+\`\`\`51+52+The default project runs local-only with durable browser storage. To enable managed sync, copy53+\`.env.example\` to \`.env\` and set both public Jazz values. Run \`deno task doctor\` before boot54+when you want configuration diagnostics without starting the application.55+56+Public tasks: \`dev\`, \`doctor\`, \`check\`, \`test\`, \`build\`, and \`preview\`.57+`;58+}59+60+function generatedDenoConfig(packagePrefix: string): string {61+ const config = {62+ imports: {63+ "@astrojs/check": "npm:@astrojs/check@0.9.9",64+ "@astrojs/preact": "npm:@astrojs/preact@6.0.1",65+ "@nzip/lofi/": packagePrefix,66+ "astro": "npm:astro@7.0.9",67+ "astro/config": "npm:astro@7.0.9/config",68+ "jazz-tools": "npm:jazz-tools@2.0.0-alpha.53",69+ "jazz-tools/dev/vite": "npm:jazz-tools@2.0.0-alpha.53/dev/vite",70+ "preact": "npm:preact@10.29.7",71+ "preact/hooks": "npm:preact@10.29.7/hooks",72+ "preact/jsx-dev-runtime": "npm:preact@10.29.7/jsx-dev-runtime",73+ "preact/jsx-runtime": "npm:preact@10.29.7/jsx-runtime",74+ "typescript": "npm:typescript@6.0.3",75+ "vite": "npm:vite@8.0.1",76+ },77+ nodeModulesDir: "auto",78+ compilerOptions: {79+ lib: ["deno.ns", "dom", "dom.iterable", "esnext"],80+ jsx: "react-jsx",81+ jsxImportSource: "preact",82+ },83+ tasks: {84+ dev: "deno run -A @nzip/lofi/dev",85+ doctor: "deno run -A @nzip/lofi/doctor",86+ check:87+ "deno fmt --check && deno lint && deno check astro.config.ts src/**/*.ts src/**/*.tsx && deno run -A npm:astro@7.0.9 check",88+ test: "deno test --allow-read --allow-write=. src/_lofi tests",89+ build: "deno run -A @nzip/lofi/build",90+ preview: "deno run -A @nzip/lofi/preview",91+ },92+ fmt: {93+ lineWidth: 100,94+ semiColons: true,95+ singleQuote: false,96+ },97+ lint: {98+ rules: { tags: ["recommended"] },99+ },100+ };101+ return `${JSON.stringify(config, null, 2)}\n`;102+}103+104+function validateName(name: string): string[] {105+ if (name.trim() !== name || name.length === 0) {106+ throw new Error("project name must be a non-empty relative path without surrounding spaces");107+ }108+ if (isAbsolute(name)) throw new Error("project name must be a relative path");109+ const segments = name.split(/[\\/]/);110+ if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {111+ throw new Error("project name cannot contain empty, '.' or '..' path segments");112+ }113+ for (const segment of segments) {114+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(segment)) {115+ throw new Error(116+ `project path segment ${117+ JSON.stringify(segment)118+ } must start with a letter or number and contain only letters, numbers, '.', '_' or '-'`,119+ );120+ }121+ }122+ return segments;123+}124+125+async function directoryIsEmpty(path: string): Promise<boolean> {126+ try {127+ for await (const _entry of Deno.readDir(path)) return false;128+ return true;129+ } catch (error) {130+ if (error instanceof Deno.errors.NotFound) return true;131+ if (error instanceof Deno.errors.NotADirectory) return false;132+ throw error;133+ }134+}135+136+async function assertNoSymlinkPath(cwd: string, segments: readonly string[]): Promise<void> {137+ let path = cwd;138+ for (const segment of segments) {139+ path = join(path, segment);140+ try {141+ const stat = await Deno.lstat(path);142+ if (stat.isSymlink) {143+ throw new Error(144+ `destination path crosses symbolic link ${145+ JSON.stringify(relative(cwd, path))146+ }; no files were changed. Choose a path without symbolic links`,147+ );148+ }149+ } catch (error) {150+ if (error instanceof Deno.errors.NotFound) return;151+ throw error;152+ }153+ }154+}155+156+async function writeTemplate(destination: string): Promise<void> {157+ for (const [relativePath, content] of Object.entries(STARTER_TEMPLATE)) {158+ const path = join(destination, relativePath);159+ await Deno.mkdir(dirname(path), { recursive: true });160+ await Deno.writeTextFile(path, content);161+ }162+}163+164+async function rewritePortableAstroConfig(root: string): Promise<void> {165+ const path = join(root, "astro.config.ts");166+ const source = await Deno.readTextFile(path);167+ const portable = source.replace(168+ 'const workspaceRoot = fileURLToPath(new URL("../../", import.meta.url));',169+ 'const workspaceRoot = fileURLToPath(new URL("./", import.meta.url));',170+ );171+ if (portable === source) throw new Error("starter Astro workspace-root marker was not found");172+ await Deno.writeTextFile(path, portable);173+}174+175+export async function createProject(options: CreateProjectOptions): Promise<CreateProjectResult> {176+ const segments = validateName(options.name);177+ const cwd = resolve(options.cwd);178+ const destination = resolve(cwd, ...segments);179+ const child = relative(cwd, destination);180+ if (child.startsWith(`..${sep}`) || child === "..") {181+ throw new Error("project destination must stay inside the current directory");182+ }183+ await assertNoSymlinkPath(cwd, segments);184+185+ if (!(await directoryIsEmpty(destination))) {186+ throw new Error(187+ `destination ${188+ JSON.stringify(options.name)189+ } already exists and is not empty; no files were changed. Choose a new name, for example: deno run -A jsr:@nzip/lofi/create ${190+ basename(options.name)191+ }-new`,192+ );193+ }194+195+ await Deno.mkdir(dirname(destination), { recursive: true });196+ const staging = await Deno.makeTempDir({197+ dir: dirname(destination),198+ prefix: `.${basename(destination)}-lofi-create-`,199+ });200+ try {201+ await writeTemplate(staging);202+ await rewritePortableAstroConfig(staging);203+ await Deno.writeTextFile(join(staging, ".gitignore"), generatedGitignore);204+ await Deno.writeTextFile(join(staging, ".env.example"), generatedEnvironment);205+ await Deno.writeTextFile(join(staging, "README.md"), generatedReadme(basename(destination)));206+ await Deno.writeTextFile(207+ join(staging, "deno.json"),208+ generatedDenoConfig(options.packagePrefix ?? LOFI_PACKAGE_PREFIX),209+ );210+211+ await Deno.remove(destination, { recursive: true }).catch((error) => {212+ if (!(error instanceof Deno.errors.NotFound)) throw error;213+ });214+ await Deno.rename(staging, destination);215+ } catch (error) {216+ await Deno.remove(staging, { recursive: true }).catch(() => undefined);217+ throw error;218+ }219+220+ return {221+ destination,222+ displayPath: options.name,223+ nextCommands: [`cd ${options.name}`, "deno task dev"],224+ };225+}@@ -0,0 +1,147 @@
1+import { join } from "node:path";2+import { createProject } from "./create_core.ts";3+4+function assert(condition: unknown, message: string): asserts condition {5+ if (!condition) throw new Error(message);6+}7+8+function assertEquals(actual: unknown, expected: unknown): void {9+ const canonical = (value: unknown): unknown => {10+ if (Array.isArray(value)) return value.map(canonical);11+ if (value !== null && typeof value === "object") {12+ return Object.fromEntries(13+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(14+ ([key, child]) => [key, canonical(child)],15+ ),16+ );17+ }18+ return value;19+ };20+ const actualJson = JSON.stringify(canonical(actual));21+ const expectedJson = JSON.stringify(canonical(expected));22+ if (actualJson !== expectedJson) {23+ throw new Error(`expected ${expectedJson}, received ${actualJson}`);24+ }25+}26+27+async function assertRejects(28+ action: () => Promise<unknown>,29+ messageIncludes: string,30+): Promise<void> {31+ try {32+ await action();33+ } catch (error) {34+ const message = error instanceof Error ? error.message : String(error);35+ assert(message.includes(messageIncludes), `expected error to include ${messageIncludes}`);36+ return;37+ }38+ throw new Error("expected promise to reject");39+}40+41+async function fileManifest(root: string): Promise<Record<string, string>> {42+ const manifest: Record<string, string> = {};43+ async function visit(path: string, prefix = "") {44+ const entries = [...Deno.readDirSync(path)].sort((left, right) =>45+ left.name.localeCompare(right.name)46+ );47+ for (const entry of entries) {48+ const relative = prefix ? `${prefix}/${entry.name}` : entry.name;49+ const child = join(path, entry.name);50+ if (entry.isDirectory) await visit(child, relative);51+ else if (entry.isFile) {52+ const digest = await crypto.subtle.digest("SHA-256", await Deno.readFile(child));53+ manifest[relative] = Array.from(new Uint8Array(digest))54+ .map((byte) => byte.toString(16).padStart(2, "0"))55+ .join("");56+ }57+ }58+ }59+ await visit(root);60+ return manifest;61+}62+63+function makeTestRoot(): Promise<string> {64+ return Deno.makeTempDir({ dir: ".", prefix: ".lofi-create-test-" });65+}66+67+Deno.test("createProject materializes the complete starter snapshot", async () => {68+ const cwd = await makeTestRoot();69+ try {70+ const result = await createProject({ cwd, name: "starter" });71+ assertEquals(result.nextCommands, ["cd starter", "deno task dev"]);72+ const actual = await fileManifest(result.destination);73+ const expected = JSON.parse(74+ await Deno.readTextFile(new URL("./testdata/starter.snapshot.json", import.meta.url)),75+ );76+ assertEquals(actual, expected);77+ const config = JSON.parse(await Deno.readTextFile(join(result.destination, "deno.json")));78+ assertEquals(config.imports["@nzip/lofi/"], "jsr:@nzip/lofi@0.1.0/");79+ } finally {80+ await Deno.remove(cwd, { recursive: true });81+ }82+});83+84+Deno.test("createProject refuses a non-empty destination without changing it", async () => {85+ const cwd = await makeTestRoot();86+ const destination = join(cwd, "taken");87+ const marker = join(destination, "keep.txt");88+ try {89+ await Deno.mkdir(destination);90+ await Deno.writeTextFile(marker, "untouched");91+ await assertRejects(() => createProject({ cwd, name: "taken" }), "no files were changed");92+ assertEquals(await Deno.readTextFile(marker), "untouched");93+ assertEquals([...Deno.readDirSync(destination)].map((entry) => entry.name), ["keep.txt"]);94+ } finally {95+ await Deno.remove(cwd, { recursive: true });96+ }97+});98+99+Deno.test("createProject accepts an existing empty directory and a local package override", async () => {100+ const cwd = await makeTestRoot();101+ try {102+ await Deno.mkdir(join(cwd, "nested", "starter"), { recursive: true });103+ const result = await createProject({104+ cwd,105+ name: "nested/starter",106+ packagePrefix: "file:///clean-room/package/",107+ });108+ const config = JSON.parse(await Deno.readTextFile(join(result.destination, "deno.json")));109+ assertEquals(config.imports["@nzip/lofi/"], "file:///clean-room/package/");110+ assert(111+ !await Array.fromAsync(Deno.readDir(result.destination)).then((entries) =>112+ entries.some((entry) => entry.name === ".env")113+ ),114+ "generated project must not contain .env",115+ );116+ } finally {117+ await Deno.remove(cwd, { recursive: true });118+ }119+});120+121+Deno.test({122+ name: "createProject refuses destinations reached through a symbolic link",123+ ignore: Deno.build.os === "windows",124+ async fn() {125+ const cwd = await makeTestRoot();126+ const outside = await makeTestRoot();127+ try {128+ const link = await new Deno.Command("ln", {129+ args: ["-s", outside, join(cwd, "linked")],130+ stdout: "null",131+ stderr: "piped",132+ }).output();133+ assert(134+ link.success,135+ `failed to create test symlink: ${new TextDecoder().decode(link.stderr)}`,136+ );137+ await assertRejects(138+ () => createProject({ cwd, name: "linked/starter" }),139+ "crosses symbolic link",140+ );141+ assertEquals([...Deno.readDirSync(outside)].length, 0);142+ } finally {143+ await Deno.remove(cwd, { recursive: true });144+ await Deno.remove(outside, { recursive: true });145+ }146+ },147+});@@ -0,0 +1,96 @@
1+import astroConfig from "../apps/reference/astro.config.ts" with { type: "text" };2+import favicon from "../apps/reference/public/favicon.svg" with { type: "text" };3+import manifest from "../apps/reference/public/manifest.webmanifest" with { type: "text" };4+import serviceWorker from "../apps/reference/public/sw.js" with { type: "text" };5+import deviceStatus from "../apps/reference/src/_lofi/DeviceStatus.tsx" with { type: "text" };6+import referenceShell from "../apps/reference/src/_lofi/ReferenceShell.astro" with { type: "text" };7+import boot from "../apps/reference/src/_lofi/boot.ts" with { type: "text" };8+import checklistStore from "../apps/reference/src/_lofi/checklist-store.ts" with { type: "text" };9+import checklistStoreTest from "../apps/reference/src/_lofi/checklist-store_test.ts" with {10+ type: "text",11+};12+import config from "../apps/reference/src/_lofi/config.ts" with { type: "text" };13+import deviceCapabilities from "../apps/reference/src/_lofi/device-capabilities.ts" with {14+ type: "text",15+};16+import probe from "../apps/reference/src/_lofi/probe.ts" with { type: "text" };17+import pwa from "../apps/reference/src/_lofi/pwa.ts" with { type: "text" };18+import pwaTest from "../apps/reference/src/_lofi/pwa_test.ts" with { type: "text" };19+import resourceLifecycle from "../apps/reference/src/_lofi/resource-lifecycle.ts" with {20+ type: "text",21+};22+import resourceLifecycleTest from "../apps/reference/src/_lofi/resource-lifecycle_test.ts" with {23+ type: "text",24+};25+import runtime from "../apps/reference/src/_lofi/runtime.ts" with { type: "text" };26+import testAssert from "../apps/reference/src/_lofi/test-assert.ts" with { type: "text" };27+import uiMutation from "../apps/reference/src/_lofi/ui-mutation.ts" with { type: "text" };28+import uiMutationTest from "../apps/reference/src/_lofi/ui-mutation_test.ts" with {29+ type: "text",30+};31+import useChecklist from "../apps/reference/src/_lofi/use-checklist.ts" with { type: "text" };32+import useDeviceCapabilities from "../apps/reference/src/_lofi/use-device-capabilities.ts" with {33+ type: "text",34+};35+import app from "../apps/reference/src/app.ts" with { type: "text" };36+import environmentTypes from "../apps/reference/src/env.d.ts" with { type: "text" };37+import checklistIsland from "../apps/reference/src/islands/ChecklistIsland.tsx" with {38+ type: "text",39+};40+import migration from "../apps/reference/src/migrations/20260715T194947-notes-to-tasks-6c62fec42c35-ff85ac1d97ee.ts" with {41+ type: "text",42+};43+import oldSchemaSnapshot from "../apps/reference/src/migrations/snapshots/20260715T194819-6c62fec42c35.json" with {44+ type: "text",45+};46+import currentSchemaSnapshot from "../apps/reference/src/migrations/snapshots/20260715T194947-ff85ac1d97ee.json" with {47+ type: "text",48+};49+import indexPage from "../apps/reference/src/pages/index.astro" with { type: "text" };50+import permissions from "../apps/reference/src/permissions.ts" with { type: "text" };51+import schema from "../apps/reference/src/schema.ts" with { type: "text" };52+import globalStyles from "../apps/reference/src/styles/global.css" with { type: "text" };53+import uiContract from "../apps/reference/src/ui-contract.ts" with { type: "text" };54+import authorBoundaryTest from "../apps/reference/tests/author-boundary_test.ts" with {55+ type: "text",56+};57+import tsconfig from "../apps/reference/tsconfig.json" with { type: "text" };58+59+/** Every source-controlled file copied from the validated reference app into a new project. */60+export const STARTER_TEMPLATE: Readonly<Record<string, string>> = {61+ "astro.config.ts": astroConfig,62+ "public/favicon.svg": favicon,63+ "public/manifest.webmanifest": manifest,64+ "public/sw.js": serviceWorker,65+ "src/_lofi/DeviceStatus.tsx": deviceStatus,66+ "src/_lofi/ReferenceShell.astro": referenceShell,67+ "src/_lofi/boot.ts": boot,68+ "src/_lofi/checklist-store.ts": checklistStore,69+ "src/_lofi/checklist-store_test.ts": checklistStoreTest,70+ "src/_lofi/config.ts": config,71+ "src/_lofi/device-capabilities.ts": deviceCapabilities,72+ "src/_lofi/probe.ts": probe,73+ "src/_lofi/pwa.ts": pwa,74+ "src/_lofi/pwa_test.ts": pwaTest,75+ "src/_lofi/resource-lifecycle.ts": resourceLifecycle,76+ "src/_lofi/resource-lifecycle_test.ts": resourceLifecycleTest,77+ "src/_lofi/runtime.ts": runtime,78+ "src/_lofi/test-assert.ts": testAssert,79+ "src/_lofi/ui-mutation.ts": uiMutation,80+ "src/_lofi/ui-mutation_test.ts": uiMutationTest,81+ "src/_lofi/use-checklist.ts": useChecklist,82+ "src/_lofi/use-device-capabilities.ts": useDeviceCapabilities,83+ "src/app.ts": app,84+ "src/env.d.ts": environmentTypes,85+ "src/islands/ChecklistIsland.tsx": checklistIsland,86+ "src/migrations/20260715T194947-notes-to-tasks-6c62fec42c35-ff85ac1d97ee.ts": migration,87+ "src/migrations/snapshots/20260715T194819-6c62fec42c35.json": oldSchemaSnapshot,88+ "src/migrations/snapshots/20260715T194947-ff85ac1d97ee.json": currentSchemaSnapshot,89+ "src/pages/index.astro": indexPage,90+ "src/permissions.ts": permissions,91+ "src/schema.ts": schema,92+ "src/styles/global.css": globalStyles,93+ "src/ui-contract.ts": uiContract,94+ "tests/author-boundary_test.ts": authorBoundaryTest,95+ "tsconfig.json": tsconfig,96+};@@ -0,0 +1,41 @@
1+{2+ ".env.example": "c22aed73c7089168ac777fd4f68ad504f9b826ea7185b705514a4ac5505918b1",3+ ".gitignore": "9efb15c6a9bfc20f296ed712e74273b6015a0dd9a40f7e861b8711dcf61825f1",4+ "README.md": "ceb7af0a5d78b71c85ecbfdf3d93bb8f46db7bbd4879eba844884164078dcf2d",5+ "astro.config.ts": "8a19cae348af7fcbd9db119d2423dd2e464f0e5ace6e5723d06cd3a1cc9a0d4b",6+ "deno.json": "99649631e408331b1d18c15e28c44219055afe52ffc645e0d9b3a9248ca1d868",7+ "public/favicon.svg": "23efdb888dae688715faafe3ef94f7b5ad1976afcc715c0a742218b54732c6ad",8+ "public/manifest.webmanifest": "d23457fe8df68a16181fde1d4ecca4b69d3b050b5fa3bf8f4051cb05bde1d3d6",9+ "public/sw.js": "d816161fd5a35f2c477ca72eaa5ba58eb8d94795a4399d29a19838e886dafa31",10+ "src/_lofi/DeviceStatus.tsx": "1ba2e2c2bee7ac15bc2830e879904deddf95bba1b55359f0376cf7cb1c9aafeb",11+ "src/_lofi/ReferenceShell.astro": "a2817146d1fe62cfdc399669709996b468823a8828257abf63b445c4c8eac912",12+ "src/_lofi/boot.ts": "dd66bd1e5ddb88599a0cbadb2c2cf932993734843c7e5cf41d28b389560a32c7",13+ "src/_lofi/checklist-store.ts": "8e9b36bc82e431ef63997915407992aa52be2a9606a023546d83b224e28cfd0e",14+ "src/_lofi/checklist-store_test.ts": "00a7bbf786b90e6bd765e3fe401331233a6fad29a9d473dc4b1f0d5146e4dbc2",15+ "src/_lofi/config.ts": "3ae7e31b3915e12eb595eeea158c0dbd026f4f0f8bf7b0f4809117bec9752f1a",16+ "src/_lofi/device-capabilities.ts": "91b03f9bed813d3770061416d40066ed6f21829877293e5395eff8f0f98fc11e",17+ "src/_lofi/probe.ts": "72c8a80561a6b5650d943456e1ce36f08d883fc1008fddee4614a2b75a8f06e9",18+ "src/_lofi/pwa.ts": "185a0668528b49eae2dd0aa905a5a2e491b3787da335d491ffecaf8bfadec430",19+ "src/_lofi/pwa_test.ts": "092d9ff442d82086a03cf45c6ed0256ab0f1ce8db42fe7e636d4b93d6ebae63e",20+ "src/_lofi/resource-lifecycle.ts": "4e017f4391f18854486044709041f163308fa3fc84e08d5ec7f45c39e3ae3cb5",21+ "src/_lofi/resource-lifecycle_test.ts": "9a43053351b8b90f3b67a2953c3ff159e0337ab8009f1d061e9e2bf7382abf43",22+ "src/_lofi/runtime.ts": "7ebbc54c66df6c92f6ecb2e6e24e131c4c1922044a86aa0fd168cdaf88a5eb19",23+ "src/_lofi/test-assert.ts": "38549889964a4e92a0d63ee514c746cae5a43e6160cea4f2075a9e61543d3744",24+ "src/_lofi/ui-mutation.ts": "3273053a796160556045d06d672f4c79d9597e1f822928f1ee71a7d31865dc32",25+ "src/_lofi/ui-mutation_test.ts": "16153da95e080570af254eb0c56433f2db8abacec542984f59f6afd4ebf1c501",26+ "src/_lofi/use-checklist.ts": "a6d2818d390d228ad2a3db554b7cb043b60f7e9a2e48636ba19247c666343eaf",27+ "src/_lofi/use-device-capabilities.ts": "f7c7b2321b400d8076a5347e0f1ff2924623535979d48a2d94a69f825b93841e",28+ "src/app.ts": "3fd3646054f11ef7a1c080445d089eba45d58df7934aeaa58edab7c965c65523",29+ "src/env.d.ts": "b44daed05ec5cdfacfd8d8acf7866974b5f6b8db923ab53bd244419093c719da",30+ "src/islands/ChecklistIsland.tsx": "77c8c085327b7832d0d9daa0f39a61d3ecf1dc6f7e50c8146499dd6d81aecbe3",31+ "src/migrations/20260715T194947-notes-to-tasks-6c62fec42c35-ff85ac1d97ee.ts": "387201d32018cf9bbc0da7041110638baf27bb9eb55a5e03f59b124bcfc336d5",32+ "src/migrations/snapshots/20260715T194819-6c62fec42c35.json": "799e32dee2ca64b7ea69ae48869f5a6dc29adfdb97dd79767a4c8697495aa582",33+ "src/migrations/snapshots/20260715T194947-ff85ac1d97ee.json": "39bb03925d68138aa5effc6fc9799de90ad4f3b6b65df84b638afc7b2d67c16b",34+ "src/pages/index.astro": "1930f00137df528f15836eb68230b06b248f8abec53f1a051a904557a1287346",35+ "src/permissions.ts": "b4580cc5f32935ac568a9aff6109c4aa7e4b3ee8cea8b28c3f306db68a55dcc2",36+ "src/schema.ts": "f2c555b088ded8eea2653f5d955fc9aae69dfd588e891adf509e99052f1e9949",37+ "src/styles/global.css": "7cbe243842f3bca32038d99a4872afe30c346cf7137387ce41861c9333a1f5c0",38+ "src/ui-contract.ts": "fc40d20dff6ba870c0de038839e66cb61d4349031e4a7748d963ca182e8bbec0",39+ "tests/author-boundary_test.ts": "7ea79031ed4ae7d7966bef2e456e02efe7d93d82c8f0bee219d885d300260ed2",40+ "tsconfig.json": "fe2e5acc66101ed42d893d7e7063d07d298927621b351adcfb1ebde6c3a0a2ac"41+}