create-june 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # create-june
2
+
3
+ Scaffold a [June](https://june.build) app — **the agent-native React framework**.
4
+
5
+ ```bash
6
+ npm create june my-app
7
+ # or: bun create june my-app
8
+ ```
9
+
10
+ One `route()` definition serves every audience:
11
+
12
+ | humans | agents |
13
+ | --- | --- |
14
+ | streamed HTML, nested layouts, loading/error boundaries | the same routes as `.md` / `.json` |
15
+ | hover prerender + view transitions, zero client JS | `llms.txt`, `sitemap.xml`, MCP tools at `/mcp` |
16
+
17
+ The scaffold also ships an `AGENTS.md` (conventions for coding agents),
18
+ plain-SQL migrations (the data layer's source of truth), and markdown content
19
+ served to agents **verbatim** — the authored file, not a lossy conversion.
20
+
21
+ > ⚠ **Pre-release.** The `june` framework package is not on npm yet; this
22
+ > scaffold tracks the framework's development and currently runs inside the
23
+ > [june repository](https://github.com/junebuild/june). Watch
24
+ > [june.build](https://june.build) for the runnable release.
25
+
26
+ MIT
package/bin.mjs ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ // create-june — scaffold a June app (the agent-native React framework).
3
+ // Non-interactive by design (CI/C3-friendly): `npm create june my-app` just works.
4
+ import { cpSync, existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
5
+ import { basename, join, resolve } from "node:path";
6
+
7
+ const args = process.argv.slice(2).filter((a) => !a.startsWith("-"));
8
+ const target = args[0] ?? "june-app";
9
+ const dest = resolve(process.cwd(), target);
10
+ const TEMPLATE = join(import.meta.dirname, "template");
11
+
12
+ if (existsSync(dest)) {
13
+ console.error(`✗ ${target} already exists — refusing to overwrite.`);
14
+ process.exit(1);
15
+ }
16
+ if (!existsSync(TEMPLATE)) {
17
+ console.error("✗ template missing from package — please file an issue.");
18
+ process.exit(1);
19
+ }
20
+
21
+ cpSync(TEMPLATE, dest, { recursive: true });
22
+
23
+ // Stamp the app name into every scaffolded file that mentions it.
24
+ const name = basename(dest);
25
+ const stamp = (dir) => {
26
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
27
+ const p = join(dir, e.name);
28
+ if (e.isDirectory()) stamp(p);
29
+ else {
30
+ const text = readFileSync(p, "utf8");
31
+ if (text.includes("__APP_NAME__")) {
32
+ writeFileSync(p, text.replaceAll("__APP_NAME__", name));
33
+ }
34
+ }
35
+ }
36
+ };
37
+ stamp(dest);
38
+
39
+ console.log(`
40
+ ✓ Created ${name}/
41
+
42
+ June is the agent-native React framework: one route() definition serves
43
+ humans (streamed HTML) AND agents (.md / .json / llms.txt / MCP tools).
44
+
45
+ ⚠ PRE-RELEASE: the \`june\` framework package is not on npm yet — this
46
+ scaffold shows the conventions and follows the framework's development.
47
+ Watch https://june.build and https://github.com/junebuild/june for the
48
+ runnable release. (The scaffold runs today inside the june repository.)
49
+
50
+ Explore:
51
+ ${target}/app/ routes, layouts, boundaries
52
+ ${target}/content/ markdown posts (served verbatim to agents as .md)
53
+ ${target}/db/ plain-SQL migrations (the data layer's truth)
54
+ ${target}/AGENTS.md conventions for coding agents
55
+ `);
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "create-june",
3
+ "version": "0.0.1",
4
+ "description": "Scaffold a June app — the agent-native React framework. Every route serves humans (streamed HTML) and agents (markdown/JSON/MCP) from one definition.",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-june": "./bin.mjs"
8
+ },
9
+ "files": [
10
+ "bin.mjs",
11
+ "template"
12
+ ],
13
+ "engines": {
14
+ "node": ">=20.11"
15
+ },
16
+ "scripts": {
17
+ "prepublishOnly": "node scripts/sync-template.mjs"
18
+ },
19
+ "keywords": [
20
+ "june",
21
+ "react",
22
+ "rsc",
23
+ "framework",
24
+ "scaffold",
25
+ "agent",
26
+ "mcp",
27
+ "dual-audience"
28
+ ],
29
+ "homepage": "https://june.build",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/junebuild/june.git"
33
+ },
34
+ "license": "MIT"
35
+ }
@@ -0,0 +1,60 @@
1
+ # __APP_NAME__ — guide for coding agents
2
+
3
+ You are working in a June app. June is dual-audience: routes serve humans (HTML)
4
+ and agents (JSON/markdown/manifests) from ONE definition. This file is the
5
+ agent-facing twin of README.md.
6
+
7
+ ## Commands
8
+ - `bun run dev` — start the dev server (hot reload)
9
+ - From the framework repo root: `bun test src/june` (framework tests), `bun run typecheck`
10
+
11
+ ## How to add things
12
+
13
+ **A page** — create `app/<path>/page.tsx` exporting a `route()`:
14
+ ```tsx
15
+ import { route } from "june/route";
16
+ export default route({
17
+ async load(ctx) { return { /* data */ }; }, // ctx.params from [segments]
18
+ view: (data) => <main>…</main>, // HTML projection
19
+ json: (data) => data, // /path.json
20
+ md: (data) => "# …", // /path.md
21
+ });
22
+ ```
23
+ Conventions per directory: `layout.tsx` (wraps), `loading.tsx` (Suspense
24
+ fallback for load()), `error.tsx` (recovery when load()/view() throws),
25
+ `not-found.tsx`, `[param]/`, `[...rest]/` (params.rest = "a/b/c"), `(group)/`
26
+ (invisible in URL), `_dir` (never routes).
27
+
28
+ **A schema change** — write a NEW file `db/migrations/000N_name.sql` (plain
29
+ SQLite SQL; DDL + backfill together; never edit an applied migration). It is
30
+ applied on next boot, in filename order. The SQL you write is exactly what runs
31
+ — there is no generator.
32
+
33
+ **A blog post / content** — drop a markdown file in `content/posts/`:
34
+ ```md
35
+ ---
36
+ title: My post
37
+ date: 2026-06-12
38
+ description: One line.
39
+ tags: [a, b]
40
+ ---
41
+ Body in plain markdown.
42
+ ```
43
+ It appears in `/blog` (list, from frontmatter) and `/blog/<filename>` (rendered).
44
+ The `.md` projection serves YOUR FILE verbatim — never edit rendered output;
45
+ edit the source file. Keep frontmatter flat (`key: value`, `[a, b]` lists only).
46
+
47
+ **An action/tool** — add a `defineAction({ id, description, input, run })` in
48
+ `app/actions.ts`. It becomes: a UI server action AND an MCP tool on `/mcp` AND
49
+ an entry in agent manifests. Write intent-shaped tools, not raw CRUD; return
50
+ high-signal shapes, not row dumps.
51
+
52
+ ## Verify your work (oracles)
53
+ - `GET /llms.txt` lists every route; `GET <route>.json` shows the data the view uses.
54
+ - `POST /mcp` `{"method":"tools/list"}` shows your tools; `tools/call` executes one.
55
+ - Writes to a table auto-invalidate route caches tagged `table:<name>` — no manual wiring.
56
+
57
+ ## Rules
58
+ - Don't bypass `route()` projections with ad-hoc endpoints — agents discover routes, not handlers.
59
+ - Don't edit applied migrations; add a new one.
60
+ - Keep `view` human-shaped and `json`/`md` agent-shaped; they share `load()` so they can't drift.
@@ -0,0 +1,41 @@
1
+ # __APP_NAME__
2
+
3
+ A [June](../../README.md) app — dual-audience by default: every route serves
4
+ humans (streamed HTML) *and* agents (JSON / markdown / agent manifests) from one
5
+ definition.
6
+
7
+ ```bash
8
+ bun run dev
9
+ ```
10
+
11
+ | try | what you get |
12
+ | --- | --- |
13
+ | `/posts` | streamed HTML inside nested layouts (loading boundary while data loads) |
14
+ | `/posts.json` | the same route as JSON |
15
+ | `/posts/hello-june.md` | the same post as markdown |
16
+ | `/llms.txt` | the agent's map of this site (auto-derived) |
17
+ | `/mcp` | your server actions as MCP tools (`createPost`) |
18
+
19
+ ## Structure
20
+
21
+ ```
22
+ june.config.ts agent surface switches (everything defaults ON)
23
+ dev.ts boot: serve(app/) + register actions
24
+ db/
25
+ migrations/0001_*.sql SQL is the structural truth — applied in order on boot
26
+ app/
27
+ layout.tsx root layout (wraps every page)
28
+ page.tsx route(): view + md projections
29
+ not-found.tsx 404, rendered inside the layout chain
30
+ models/post.ts domain model (june/db sqlite) + migration runner
31
+ actions.ts server action == MCP tool (one definition)
32
+ posts/
33
+ loading.tsx Suspense fallback while load() runs
34
+ page.tsx route(): view + json + md, cached, tag-invalidated
35
+ [slug]/
36
+ page.tsx dynamic segment
37
+ error.tsx recovery UI when load() throws
38
+ ```
39
+
40
+ Conventions reference: `docs/app-router.md`. Data-layer philosophy (why
41
+ migrations are plain SQL): `docs/data-philosophy.md`.
@@ -0,0 +1,28 @@
1
+ // A server action that is ALSO an agent tool: the UI posts it, /mcp lists and
2
+ // executes it, the agent manifest advertises it. One definition, every surface.
3
+ import { defineAction } from "june/agent";
4
+ import { Posts } from "./models/post";
5
+
6
+ export const createPost = defineAction({
7
+ id: "createPost",
8
+ description: "Publish a new post with a unique slug, a title, and a markdown body.",
9
+ input: {
10
+ type: "object",
11
+ properties: {
12
+ slug: { type: "string", description: "URL slug, e.g. my-first-post" },
13
+ title: { type: "string", description: "Post title" },
14
+ body: { type: "string", description: "Markdown body" },
15
+ },
16
+ required: ["slug", "title"],
17
+ },
18
+ async run(input: { slug: string; title: string; body?: string }) {
19
+ // Writing `posts` auto-invalidates every cache entry tagged table:posts —
20
+ // the /posts list cache below drops without manual wiring.
21
+ return Posts.insert({
22
+ slug: input.slug,
23
+ title: input.title,
24
+ body: input.body ?? "",
25
+ published: 1,
26
+ });
27
+ },
28
+ });
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import { join } from "node:path";
3
+ import { route } from "june/route";
4
+ import { entry } from "june/content";
5
+
6
+ const POSTS = join(import.meta.dirname, "../../../content/posts");
7
+
8
+ export default route({
9
+ metadata: ({ post }) => ({
10
+ title: String(post.data.title ?? post.slug),
11
+ description: String(post.data.description ?? ""),
12
+ openGraph: { type: "article" },
13
+ }),
14
+ async load(ctx) {
15
+ const post = entry(POSTS, ctx.params.slug);
16
+ if (!post) throw new Error(`No post "${ctx.params.slug}"`);
17
+ return { post };
18
+ },
19
+ view: ({ post }) => (
20
+ <article>
21
+ <h1>{String(post.data.title)}</h1>
22
+ <p><small>{String(post.data.date)}</small></p>
23
+ <div dangerouslySetInnerHTML={{ __html: post.html }} />
24
+ <p><a href="/blog">← all posts</a></p>
25
+ </article>
26
+ ),
27
+ // THE differentiator: the .md projection is the AUTHORED FILE, verbatim.
28
+ md: ({ post }) => post.original,
29
+ json: ({ post }) => ({ slug: post.slug, ...post.data, body: post.body }),
30
+ });
@@ -0,0 +1,30 @@
1
+ import React from "react";
2
+ import { join } from "node:path";
3
+ import { route } from "june/route";
4
+ import { collection } from "june/content";
5
+
6
+ const POSTS = join(import.meta.dirname, "../../content/posts");
7
+
8
+ export default route({
9
+ metadata: { title: "Blog", description: "Posts — rendered for humans, verbatim markdown for agents." },
10
+ load: () => ({ posts: collection(POSTS) }),
11
+ view: ({ posts }) => (
12
+ <main>
13
+ <h1>Blog</h1>
14
+ <ul>
15
+ {posts.map((p) => (
16
+ <li key={p.slug} style={{ marginBottom: 8 }}>
17
+ <a href={`/blog/${p.slug}`}>{String(p.data.title)}</a>
18
+ <br />
19
+ <small>{String(p.data.date)} — {String(p.data.description ?? "")}</small>
20
+ </li>
21
+ ))}
22
+ </ul>
23
+ </main>
24
+ ),
25
+ json: ({ posts }) => ({
26
+ posts: posts.map((p) => ({ slug: p.slug, ...p.data })),
27
+ }),
28
+ md: ({ posts }) =>
29
+ "# Blog\n\n" + posts.map((p) => `- [${p.data.title}](/blog/${p.slug}) — ${p.data.date}`).join("\n") + "\n",
30
+ });
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+
3
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
4
+ return (
5
+ <div data-layout="root">
6
+ <nav style={{ display: "flex", gap: 16, padding: "14px 20px", borderBottom: "1px solid #e4e2da" }}>
7
+ <a href="/" style={{ fontWeight: 600 }}>__APP_NAME__</a>
8
+ <a href="/posts">Posts</a>
9
+ <a href="/llms.txt">llms.txt</a>
10
+ </nav>
11
+ {children}
12
+ </div>
13
+ );
14
+ }
@@ -0,0 +1,35 @@
1
+ // Domain model — a model is a domain concern, not a route concern. Both
2
+ // /posts and /posts/[slug] import it.
3
+ import { readdirSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { sql, type Table } from "june/db";
6
+
7
+ export type Post = {
8
+ id: number;
9
+ slug: string;
10
+ title: string;
11
+ body: string;
12
+ published: number;
13
+ created_at: string;
14
+ };
15
+
16
+ const root = join(import.meta.dirname, "../..");
17
+ const db = sql.load(join(root, "db/app.db"));
18
+
19
+ // db/migrations/*.sql is the structural truth (docs/data-philosophy.md):
20
+ // applied in filename order, tracked in _migrations. No diff engine — the SQL
21
+ // you read is the SQL that ran.
22
+ const dir = join(root, "db/migrations");
23
+ db.db.run("CREATE TABLE IF NOT EXISTS _migrations (name TEXT PRIMARY KEY)");
24
+ for (const f of readdirSync(dir).filter((f) => f.endsWith(".sql")).sort()) {
25
+ const applied = db.db.query("SELECT 1 FROM _migrations WHERE name = ?").get(f);
26
+ if (applied) continue;
27
+ db.db.transaction(() => {
28
+ // exec(), not run(): migration files are MULTI-statement (DDL + seed DML);
29
+ // prepare()-based run executes only the first statement on node:sqlite.
30
+ db.db.exec(readFileSync(join(dir, f), "utf8"));
31
+ db.db.run("INSERT INTO _migrations (name) VALUES (?)", [f]);
32
+ })();
33
+ }
34
+
35
+ export const Posts = db.posts as Table<Post>;
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+
3
+ export default function NotFound({ pathname }: { pathname: string }) {
4
+ return (
5
+ <main data-boundary="not-found">
6
+ <h1>Not found</h1>
7
+ <p>
8
+ Nothing lives at <code>{pathname}</code>. Try <a href="/posts">/posts</a>.
9
+ </p>
10
+ </main>
11
+ );
12
+ }
@@ -0,0 +1,18 @@
1
+ import React from "react";
2
+ import { route } from "june/route";
3
+
4
+ export default route({
5
+ view: () => (
6
+ <main>
7
+ <h1>__APP_NAME__</h1>
8
+ <p>A June app. Humans get this page; agents get the same routes as data:</p>
9
+ <ul>
10
+ <li><code>/posts.json</code> — the list as JSON</li>
11
+ <li><code>/posts/hello-june.md</code> — a post as markdown</li>
12
+ <li><code>/llms.txt</code> · <code>/mcp</code> — discovery + tools</li>
13
+ </ul>
14
+ </main>
15
+ ),
16
+ md: () =>
17
+ "# __APP_NAME__\n\nA June app. Routes speak HTML to humans and JSON/markdown/manifests to agents.\n",
18
+ });
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+
3
+ export default function PostError({ error }: { error: unknown }) {
4
+ return (
5
+ <main data-boundary="error">
6
+ <h1>Could not load this post</h1>
7
+ <p>
8
+ <code>{String((error as Error)?.message ?? error)}</code>
9
+ </p>
10
+ <p>
11
+ <a href="/posts">Back to all posts</a>
12
+ </p>
13
+ </main>
14
+ );
15
+ }
@@ -0,0 +1,23 @@
1
+ import React from "react";
2
+ import { route } from "june/route";
3
+ import { Posts } from "../../models/post";
4
+
5
+ export default route({
6
+ metadata: ({ post }) => ({ title: post.title, openGraph: { type: "article" } }),
7
+ async load(ctx) {
8
+ const post = await Posts.findBy({ slug: ctx.params.slug, published: 1 });
9
+ if (!post) throw new Error(`No post named "${ctx.params.slug}"`);
10
+ return { post };
11
+ },
12
+ view: ({ post }) => (
13
+ <article>
14
+ <h1>{post.title}</h1>
15
+ <p>{post.body}</p>
16
+ <p>
17
+ <small>{post.created_at}</small> · <a href="/posts">all posts</a>
18
+ </p>
19
+ </article>
20
+ ),
21
+ json: ({ post }) => post,
22
+ md: ({ post }) => `# ${post.title}\n\n${post.body}\n`,
23
+ });
@@ -0,0 +1,5 @@
1
+ import React from "react";
2
+
3
+ export default function Loading() {
4
+ return <p data-boundary="loading">Loading posts…</p>;
5
+ }
@@ -0,0 +1,26 @@
1
+ import React from "react";
2
+ import { route } from "june/route";
3
+ import { Posts } from "../models/post";
4
+
5
+ export default route({
6
+ metadata: { title: "Posts", description: "All published posts." },
7
+ async load() {
8
+ return { posts: await Posts.where({ published: 1 }).all() };
9
+ },
10
+ view: ({ posts }) => (
11
+ <main>
12
+ <h1>Posts</h1>
13
+ <ul>
14
+ {posts.map((p) => (
15
+ <li key={p.id}>
16
+ <a href={`/posts/${p.slug}`}>{p.title}</a>
17
+ </li>
18
+ ))}
19
+ </ul>
20
+ </main>
21
+ ),
22
+ json: ({ posts }) => ({ posts: posts.map(({ id, slug, title }) => ({ id, slug, title })) }),
23
+ md: ({ posts }) => "# Posts\n\n" + posts.map((p) => `- [${p.title}](/posts/${p.slug})`).join("\n") + "\n",
24
+ // Cached until a write to `posts` invalidates the tag (e.g. the createPost tool).
25
+ cache: { ttl: 60, tags: ["table:posts"] },
26
+ });
@@ -0,0 +1,9 @@
1
+ ---
2
+ title: Writing with agents
3
+ date: 2026-06-09
4
+ description: Notes on a blog whose first reader is a crawler with taste.
5
+ tags: [agents]
6
+ ---
7
+
8
+ Half the readers of this blog will be machines. June treats that as a feature:
9
+ every post advertises itself in `llms.txt` and serves clean markdown.
@@ -0,0 +1,17 @@
1
+ ---
2
+ title: Content as files, audiences as projections
3
+ date: 2026-06-10
4
+ description: One markdown file. Humans get rendered HTML; agents get the file itself.
5
+ tags: [content, dual-audience]
6
+ ---
7
+
8
+ This post is a plain markdown file in `content/posts/`.
9
+
10
+ - You are reading the **rendered HTML** projection.
11
+ - An agent fetching this URL with `.md` gets the **authored file, verbatim** —
12
+ frontmatter included. No lossy HTML-to-markdown conversion.
13
+
14
+ ```ts
15
+ // the whole route:
16
+ const post = entry(POSTS, ctx.params.slug);
17
+ ```
@@ -0,0 +1,15 @@
1
+ -- SQL migrations are the structural truth (see docs/data-philosophy.md).
2
+ -- DDL + seed (DML) co-located, applied in order, tracked in _migrations.
3
+ CREATE TABLE posts (
4
+ id INTEGER PRIMARY KEY,
5
+ slug TEXT NOT NULL UNIQUE,
6
+ title TEXT NOT NULL,
7
+ body TEXT NOT NULL DEFAULT '',
8
+ published INTEGER NOT NULL DEFAULT 0,
9
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
10
+ );
11
+
12
+ INSERT INTO posts (slug, title, body, published) VALUES
13
+ ('hello-june', 'Hello, June', 'This post was created by db/migrations/0001 — plain SQL is the structural truth.', 1),
14
+ ('dual-audience', 'Dual-audience by default', 'Every route here also speaks JSON, markdown, and agent manifests. Try /posts.json or /posts/hello-june.md.', 1),
15
+ ('draft-notes', 'Draft notes', 'Unpublished drafts stay out of every list and projection.', 0);
@@ -0,0 +1,14 @@
1
+ import { join } from "node:path";
2
+ import { serve } from "june/server";
3
+ import config from "./june.config";
4
+ import "./app/actions"; // register actions (= agent tools) before the first request
5
+
6
+ const server = serve({
7
+ appDir: join(import.meta.dirname, "app"),
8
+ pageConvention: true,
9
+ agent: config.agent,
10
+ });
11
+
12
+ console.log(`__APP_NAME__ on http://localhost:${server.port}`);
13
+ console.log("human: / /posts /posts/hello-june");
14
+ console.log("agent: /posts.json /posts/hello-june.md /llms.txt /mcp");
@@ -0,0 +1,22 @@
1
+ import { defineJune } from "june/config";
2
+
3
+ // Dual-audience is ON by default — this file exists to turn things off, not on.
4
+ // agent.discovery llms.txt, sitemap.xml, api-catalog, Link header
5
+ // agent.mcp the /mcp endpoint (your actions as tools)
6
+ // agent.webmcp in-page tools for browser-resident agents
7
+ export default defineJune({
8
+ // Site-wide <head> defaults; per-route `metadata` merges over these.
9
+ site: { name: "__APP_NAME__", titleTemplate: "%s — __APP_NAME__" },
10
+ agent: { enabled: true, discovery: true, mcp: true, webmcp: true },
11
+ // Hover-intent speculation (these ARE the defaults — shown so you can see
12
+ // and change them). Agent surfaces (*.md/*.json/*.txt/*.xml//mcp) are always
13
+ // excluded. Heavy pages? prerender: "conservative" (mousedown) or false.
14
+ speculation: { prerender: "moderate", prefetch: "moderate", exclude: [] },
15
+ // Cross-document View Transitions: MPA navigations cross-fade, zero JS.
16
+ // (Also a default — visible here so you can turn it off.)
17
+ viewTransitions: true,
18
+ // Early Hints (RFC 8297): list critical assets to preload while the server
19
+ // renders, e.g. "</fonts/inter.woff2>; rel=preload; as=font; crossorigin".
20
+ // CF upgrades the Link header to a real 103; June's Node host emits 103 itself.
21
+ earlyHints: [],
22
+ });
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "__APP_NAME__",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "bun --hot dev.ts"
7
+ },
8
+ "dependencies": {
9
+ "react": "^19.0.0"
10
+ }
11
+ }