@timber-js/app 0.2.0-alpha.154 → 0.2.0-alpha.155

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.
Files changed (42) hide show
  1. package/agent-skill.md +344 -0
  2. package/dist/cli.d.ts +9 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +41 -4
  5. package/dist/cli.js.map +1 -1
  6. package/docs/api/30-api-server.mdx +233 -0
  7. package/docs/api/31-api-client.mdx +161 -0
  8. package/docs/api/32-api-cache.mdx +193 -0
  9. package/docs/api/33-api-search-params.mdx +141 -0
  10. package/docs/api/34-api-config.mdx +189 -0
  11. package/docs/api/35-api-typescript.mdx +170 -0
  12. package/docs/api/36-cli.mdx +143 -0
  13. package/docs/learn/00-introduction.mdx +81 -0
  14. package/docs/learn/01-quick-start.mdx +94 -0
  15. package/docs/learn/02-pages-and-layouts.mdx +138 -0
  16. package/docs/learn/03-fetching-data.mdx +211 -0
  17. package/docs/learn/03b-access-control.mdx +130 -0
  18. package/docs/learn/04-loading-states.mdx +67 -0
  19. package/docs/learn/04b-the-flush-point.mdx +115 -0
  20. package/docs/learn/05-typed-params.mdx +275 -0
  21. package/docs/learn/06-forms-and-actions.mdx +150 -0
  22. package/docs/learn/08-streaming.mdx +146 -0
  23. package/docs/learn/09-caching.mdx +121 -0
  24. package/docs/learn/10-middleware.mdx +156 -0
  25. package/docs/learn/11-error-handling.mdx +147 -0
  26. package/docs/learn/12-client-navigation.mdx +168 -0
  27. package/docs/learn/13-configuration.mdx +117 -0
  28. package/docs/learn/14-deploying.mdx +206 -0
  29. package/docs/more/01-advanced-routing.mdx +139 -0
  30. package/docs/more/02-advanced-forms.mdx +137 -0
  31. package/docs/more/03-coming-from-nextjs.mdx +186 -0
  32. package/docs/more/04-metadata-and-fonts.mdx +193 -0
  33. package/docs/more/04b-mdx.mdx +209 -0
  34. package/docs/more/05-content-collections.mdx +90 -0
  35. package/docs/more/06-instrumentation.mdx +188 -0
  36. package/docs/more/07-security.mdx +123 -0
  37. package/docs/more/40-why-timber.mdx +50 -0
  38. package/docs/more/41-timber-vs-nextjs.mdx +81 -0
  39. package/docs/more/42-timber-vs-others.mdx +68 -0
  40. package/llms.txt +55 -0
  41. package/package.json +6 -2
  42. package/src/cli.ts +62 -3
package/agent-skill.md ADDED
@@ -0,0 +1,344 @@
1
+ # timber.js — AI Agent Skill
2
+
3
+ Use this skill when building, modifying, or debugging a timber.js application. timber.js is a Vite-native React framework with file-system routing, React Server Components, and real HTTP status codes.
4
+
5
+ timber.js is inspired by, but is NOT Next.js. Do not assume Next.js features, APIs, or conventions work in timber. The routing structure is similar, but the APIs, rendering model, and conventions diverge significantly. Always consult timber docs, not Next.js docs.
6
+
7
+ Full documentation is available in `node_modules/@timber-js/app/docs/` (MDX files). Read the relevant doc before implementing a feature. This skill covers the essential patterns — the docs go deeper.
8
+
9
+ ## Project structure
10
+
11
+ ```
12
+ app/
13
+ layout.tsx # Root layout (required) — wraps all pages
14
+ page.tsx # Home page (/)
15
+ globals.css # Global styles
16
+ about/
17
+ page.tsx # /about
18
+ blog/
19
+ layout.tsx # Blog layout — wraps blog pages
20
+ page.tsx # /blog
21
+ [slug]/
22
+ page.tsx # /blog/:slug
23
+ timber.config.ts # Framework config (output mode, adapter, cache)
24
+ vite.config.ts # Vite config — imports timber()
25
+ tsconfig.json
26
+ ```
27
+
28
+ ## Segment convention files
29
+
30
+ Each directory in `app/` is a **segment**. These files have special meaning:
31
+
32
+ | File | Purpose |
33
+ | --------------- | ------------------------------------------------------------- |
34
+ | `page.tsx` | Route leaf — renders at this URL |
35
+ | `layout.tsx` | Persistent shell — wraps children, survives client navigation |
36
+ | `access.ts` | Auth gate — runs inside React tree, supports slot degradation |
37
+ | `middleware.ts` | Pre-render hook — headers, redirects, cache warming |
38
+ | `schema.ts` | Segment & search param codecs |
39
+ | `error.tsx` | Error boundary for this subtree |
40
+ | `404.tsx` | Custom 404 page (must be a client component) |
41
+
42
+ There is NO `loading.tsx`. Use `<Suspense>` with the flush point model instead.
43
+
44
+ ## Key imports
45
+
46
+ ```ts
47
+ // Server (server components, middleware, access, actions)
48
+ import { deny, redirect, getHeaders, getCookieJar, getSegmentParams,
49
+ createActionClient, ActionError, revalidatePath, revalidateTag,
50
+ waitUntil, getTraceId, withSpan } from '@timber-js/app/server';
51
+
52
+ // Client (client components)
53
+ import { Link, useRouter, usePathname, useActionState,
54
+ useSegmentParams, usePendingNavigation, useLinkStatus,
55
+ useSelectedLayoutSegment } from '@timber-js/app/client';
56
+
57
+ // Typed params
58
+ import { defineSchema } from '@timber-js/app/params';
59
+ import { codec } from '@timber-js/app/codec';
60
+ import { defineSearchParams } from '@timber-js/app/search-params';
61
+ import { defineCookie } from '@timber-js/app/cookies';
62
+
63
+ // Caching
64
+ import { cache } from '@timber-js/app/cache';
65
+
66
+ // Generated segment module (typed params, segment path)
67
+ import { SEGMENT_PATH } from './$segment';
68
+ ```
69
+
70
+ ## Pages and layouts
71
+
72
+ Pages are server components by default — async, server-only, zero client JS:
73
+
74
+ ```tsx
75
+ // app/products/[id]/page.tsx
76
+ import { SEGMENT_PATH } from './$segment';
77
+ import { deny, getSegmentParams } from '@timber-js/app/server';
78
+
79
+ export default async function ProductPage() {
80
+ const { id } = getSegmentParams(SEGMENT_PATH);
81
+ const product = await db.products.find(id);
82
+ if (!product) return deny(404); // Real HTTP 404
83
+ return <h1>{product.name}</h1>;
84
+ }
85
+ ```
86
+
87
+ Layouts receive `{ children }` and persist across navigation:
88
+
89
+ ```tsx
90
+ // app/layout.tsx — root layout (required)
91
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
92
+ return (
93
+ <html lang="en">
94
+ <body>{children}</body>
95
+ </html>
96
+ );
97
+ }
98
+ ```
99
+
100
+ ## Server and client components
101
+
102
+ **Server components** (default): async, access secrets/DB, zero client JS cost.
103
+ **Client components** (`'use client'`): `useState`, `useEffect`, event handlers, browser APIs.
104
+
105
+ ```tsx
106
+ // app/counter.tsx
107
+ 'use client';
108
+ import { useState } from 'react';
109
+ export function Counter() {
110
+ const [count, setCount] = useState(0);
111
+ return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
112
+ }
113
+ ```
114
+
115
+ Server components can render client components as children. Client components cannot import server components (pass them as `children` or props instead).
116
+
117
+ ## deny() and redirect()
118
+
119
+ `deny()` produces real HTTP status codes. `redirect()` sends HTTP redirects.
120
+
121
+ ```ts
122
+ deny(); // 403 (default)
123
+ deny(404); // 404 Not Found
124
+ deny(503, data); // 503 with data for error boundary
125
+
126
+ redirect('/login'); // 307 temporary
127
+ redirect('/new-page', { permanent: true }); // 308 permanent
128
+ ```
129
+
130
+ ## Forms and server actions
131
+
132
+ Actions use `createActionClient` for reusable middleware:
133
+
134
+ ```ts
135
+ // lib/action.ts
136
+ 'use server';
137
+ import { createActionClient, ActionError } from '@timber-js/app/server';
138
+
139
+ export const actionClient = createActionClient({
140
+ middleware: async () => {
141
+ const user = await getUser();
142
+ if (!user) throw new ActionError('UNAUTHORIZED');
143
+ return { user };
144
+ },
145
+ });
146
+ ```
147
+
148
+ ```ts
149
+ // app/todos/actions.ts
150
+ 'use server';
151
+ import { z } from 'zod/v4';
152
+ import { action } from '@/lib/action';
153
+ import { revalidatePath } from '@timber-js/app/server';
154
+
155
+ export const createTodo = actionClient
156
+ .schema(z.object({ title: z.string().min(1) }))
157
+ .action(async ({ input, ctx }) => {
158
+ await db.todos.create({ ...input, userId: ctx.user.id });
159
+ return revalidatePath('/todos');
160
+ });
161
+ ```
162
+
163
+ ```tsx
164
+ // app/todos/todo-form.tsx
165
+ 'use client';
166
+ import { useActionState } from '@timber-js/app/client';
167
+ import { createTodo } from './actions';
168
+
169
+ export function TodoForm() {
170
+ const [state, formAction, pending, errors] = useActionState(createTodo, null);
171
+ return (
172
+ <form action={formAction}>
173
+ <input name="title" required />
174
+ {errors.getFieldError('title') && <p>{errors.getFieldError('title')}</p>}
175
+ <button disabled={pending}>{pending ? 'Adding...' : 'Add'}</button>
176
+ </form>
177
+ );
178
+ }
179
+ ```
180
+
181
+ Forms work without JavaScript — they submit as standard POST. With JS, `useActionState` enhances with pending state.
182
+
183
+ ## Access control (access.ts)
184
+
185
+ ```ts
186
+ // app/(authenticated)/access.ts
187
+ import { getCookieJar, redirect } from '@timber-js/app/server';
188
+
189
+ export default async function access() {
190
+ const session = getSession(getCookieJar());
191
+ if (!session) return redirect('/login');
192
+ }
193
+ ```
194
+
195
+ Access runs inside the React tree. It shares `React.cache` scope with layouts. For parallel routes (slots), denied slots degrade gracefully while the rest of the page renders.
196
+
197
+ ## Middleware
198
+
199
+ Two layers: `proxy.ts` (global, has `next()`) and `middleware.ts` (per-segment, no `next()`).
200
+
201
+ ```ts
202
+ // app/dashboard/middleware.ts
203
+ import { redirect } from '@timber-js/app/server';
204
+ import type { MiddlewareContext } from '@timber-js/app/server';
205
+
206
+ export default async function middleware(ctx: MiddlewareContext) {
207
+ const session = await getSession(ctx.req);
208
+ if (!session) redirect('/login');
209
+ ctx.headers.set('x-custom', 'value'); // Set response headers
210
+ }
211
+ ```
212
+
213
+ Use middleware for headers, redirects, cache warming. Use `access.ts` for auth.
214
+
215
+ ## Typed params
216
+
217
+ Define codecs in `app/schema.ts`:
218
+
219
+ ```ts
220
+ // app/schema.ts
221
+ import { defineSchema } from '@timber-js/app/params';
222
+ import { codec } from '@timber-js/app/codec';
223
+
224
+ export default defineSchema({
225
+ segmentParams: {
226
+ '[id]': codec.integer,
227
+ '[slug]': codec.string,
228
+ },
229
+ });
230
+ ```
231
+
232
+ ## Search params
233
+
234
+ ```ts
235
+ // app/products/search-params.ts
236
+ import { defineSearchParams } from '@timber-js/app/search-params';
237
+ import { codec } from '@timber-js/app/codec';
238
+
239
+ export const { searchParams } = defineSearchParams({
240
+ q: codec.string.optional,
241
+ page: codec.integer.default(1),
242
+ });
243
+ ```
244
+
245
+ Server: `searchParams.get()`. Client: `searchParams.useQueryStates()`.
246
+
247
+ ## Caching
248
+
249
+ No implicit caching. `fetch()` is never patched. Use `timber.cache()` for cross-request caching:
250
+
251
+ ```ts
252
+ import { cache } from '@timber-js/app/cache';
253
+
254
+ const getProducts = cache(
255
+ async () => db.products.findMany(),
256
+ { ttl: 60, tags: ['products'] }
257
+ );
258
+ ```
259
+
260
+ Use `React.cache` for single-request deduplication.
261
+
262
+ ## The flush point
263
+
264
+ timber holds the HTTP response until the shell (everything outside `<Suspense>`) resolves. This means:
265
+
266
+ - Data fetched outside `<Suspense>` can affect the status code (`deny(404)` works)
267
+ - Data inside `<Suspense>` streams after the shell flushes
268
+ - There is no `loading.tsx` — use `<Suspense>` with a fallback explicitly
269
+
270
+ ## Configuration
271
+
272
+ ```ts
273
+ // timber.config.ts
274
+ import { cloudflare } from '@timber-js/app/adapters/cloudflare';
275
+
276
+ export default {
277
+ output: 'server', // or 'static'
278
+ adapter: cloudflare(), // or nitro({ preset: 'node-server' })
279
+ };
280
+ ```
281
+
282
+ ```ts
283
+ // vite.config.ts
284
+ import { defineConfig } from 'vite';
285
+ import { timber } from '@timber-js/app';
286
+
287
+ export default defineConfig({
288
+ plugins: [timber()],
289
+ });
290
+ ```
291
+
292
+ ## Client navigation
293
+
294
+ ```tsx
295
+ import { Link } from '@timber-js/app/client';
296
+
297
+ <Link href="/about">About</Link>
298
+ <Link href="/products/123" prefetch>Product</Link>
299
+ ```
300
+
301
+ ```tsx
302
+ const router = useRouter();
303
+ router.push('/dashboard');
304
+ router.replace('/settings');
305
+ router.refresh();
306
+ ```
307
+
308
+ ## Route patterns
309
+
310
+ - `(group)/` — route group (no URL segment)
311
+ - `_private/` — excluded from routing
312
+ - `[param]/` — dynamic segment
313
+ - `[...catchAll]/` — catch-all segment
314
+ - `@slot/` — parallel route (named slot)
315
+ - `(.)path/` — intercepting route
316
+
317
+ ## Common mistakes to avoid
318
+
319
+ 1. **No `loading.tsx`** — timber doesn't have it. Use `<Suspense>` explicitly.
320
+ 2. **Don't patch fetch** — `fetch()` is never cached. Use `timber.cache()` or `React.cache`.
321
+ 3. **`deny()` not `throw new Error`** — use `deny(404)` for proper status codes, not error throwing.
322
+ 4. **Server actions need `'use server'`** — at the top of the file or the function.
323
+ 5. **`access.ts` exports a default function** — not a named export.
324
+ 6. **Root layout must include `<html>` and `<body>`** — timber doesn't add these.
325
+ 7. **`getSegmentParams` uses ALS** — works in server components, middleware, access, actions. No prop drilling needed.
326
+ 8. **`useActionState` returns 4 values** — `[state, formAction, isPending, errors]`. The 4th is timber-specific with `errors.getFieldError()`, `errors.fieldErrors`, `errors.serverError`.
327
+ 9. **Middleware has one arg** — `middleware(ctx: MiddlewareContext)`, not `(req, res)`.
328
+ 10. **Don't use `next/` imports** — use `@timber-js/app/server`, `@timber-js/app/client`, etc.
329
+
330
+ ## Commands
331
+
332
+ ```bash
333
+ pnpm dev # Start dev server
334
+ pnpm build # Production build
335
+ pnpm preview # Preview production build
336
+ ```
337
+
338
+ ## Further reading
339
+
340
+ Full docs are in `node_modules/@timber-js/app/docs/`. Key files:
341
+
342
+ - `docs/learn/` — Core concepts (pages, data fetching, forms, caching, middleware)
343
+ - `docs/more/` — Advanced topics (routing, metadata, MDX, content collections, security)
344
+ - `docs/api/` — API reference for every export
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- declare const COMMANDS: readonly ["dev", "build", "preview", "check", "schema"];
2
+ declare const COMMANDS: readonly ["dev", "build", "preview", "check", "schema", "init"];
3
3
  type Command = (typeof COMMANDS)[number];
4
4
  export interface ParsedArgs {
5
5
  command: Command;
@@ -70,5 +70,13 @@ export declare function runCheck(options: CommandOptions): Promise<void>;
70
70
  * See design/41-global-params.md §Auto-Scaffolding
71
71
  */
72
72
  export declare function runSchema(args: string[]): Promise<void>;
73
+ /**
74
+ * `timber init` — write AGENTS.md (and CLAUDE.md pointer) to the project root
75
+ * so AI coding agents can discover timber.js documentation and conventions.
76
+ *
77
+ * The content is read from the installed @timber-js/app package itself, so it
78
+ * stays in sync with the installed version.
79
+ */
80
+ export declare function runInit(): Promise<void>;
73
81
  export {};
74
82
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAaA,QAAA,MAAM,QAAQ,yDAA0D,CAAC;AACzE,KAAK,OAAO,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzC,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAsCpD;AAID,kDAAkD;AAClD,MAAM,WAAW,QAAQ;IACvB,YAAY,CAAC,EAAE,cAAc,MAAM,EAAE,YAAY,CAAC;IAClD,aAAa,CAAC,EAAE,cAAc,MAAM,EAAE,aAAa,CAAC;IACpD,OAAO,CAAC,EAAE,cAAc,MAAM,EAAE,OAAO,CAAC;CACzC;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAOrF;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAMvF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,kBAAkB,EAAE,qBAAqB,GAAG,SAAS,GACpE,SAAS,GAAG,MAAM,CAKpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBzF;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAerE;AAID;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA6C7D"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAaA,QAAA,MAAM,QAAQ,iEAAkE,CAAC;AACjF,KAAK,OAAO,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzC,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,6EAA6E;IAC7E,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CAsCpD;AAID,kDAAkD;AAClD,MAAM,WAAW,QAAQ;IACvB,YAAY,CAAC,EAAE,cAAc,MAAM,EAAE,YAAY,CAAC;IAClD,aAAa,CAAC,EAAE,cAAc,MAAM,EAAE,aAAa,CAAC;IACpD,OAAO,CAAC,EAAE,cAAc,MAAM,EAAE,OAAO,CAAC;CACzC;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAOrF;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAMvF;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,kBAAkB,EAAE,qBAAqB,GAAG,SAAS,GACpE,SAAS,GAAG,MAAM,CAKpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBzF;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAerE;AAID;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA6C7D;AAID;;;;;;GAMG;AACH,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CA6C7C"}
package/dist/cli.js CHANGED
@@ -5,7 +5,8 @@ var COMMANDS = [
5
5
  "build",
6
6
  "preview",
7
7
  "check",
8
- "schema"
8
+ "schema",
9
+ "init"
9
10
  ];
10
11
  /**
11
12
  * Parse CLI arguments into a structured command + options.
@@ -17,7 +18,7 @@ var COMMANDS = [
17
18
  * goes through the PORT env var or server.port in vite.config.ts.
18
19
  */
19
20
  function parseArgs(args) {
20
- if (args.length === 0) throw new Error("No command provided. Usage: timber <dev|build|preview|check|schema> [--config <path>]");
21
+ if (args.length === 0) throw new Error("No command provided. Usage: timber <dev|build|preview|check|schema|init> [--config <path>]");
21
22
  const command = args[0];
22
23
  if (!COMMANDS.includes(command)) throw new Error(`Unknown command: ${command}. Available commands: ${COMMANDS.join(", ")}`);
23
24
  let config;
@@ -28,7 +29,7 @@ function parseArgs(args) {
28
29
  config = args[++i];
29
30
  if (!config) throw new Error("--config requires a path argument");
30
31
  } else if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}. The timber CLI only accepts --config <path>.${arg === "--port" || arg === "-p" ? " To change the port, set the PORT environment variable (PORT=4000 timber dev) or server.port in vite.config.ts." : ""}`);
31
- else if (command === "schema") rest.push(arg);
32
+ else if (command === "schema" || command === "init") rest.push(arg);
32
33
  else throw new Error(`Unexpected argument: ${arg}. Usage: timber ${command} [--config <path>]`);
33
34
  }
34
35
  return {
@@ -138,6 +139,39 @@ async function runSchema(args) {
138
139
  for (const seg of result.removedSegments) console.log(` - ${seg}`);
139
140
  }
140
141
  }
142
+ /**
143
+ * `timber init` — write AGENTS.md (and CLAUDE.md pointer) to the project root
144
+ * so AI coding agents can discover timber.js documentation and conventions.
145
+ *
146
+ * The content is read from the installed @timber-js/app package itself, so it
147
+ * stays in sync with the installed version.
148
+ */
149
+ async function runInit() {
150
+ const { readFileSync, writeFileSync, existsSync } = await import("node:fs");
151
+ const { resolve, dirname } = await import("node:path");
152
+ const { fileURLToPath } = await import("node:url");
153
+ const root = process.cwd();
154
+ const agentSkillPath = resolve(resolve(dirname(fileURLToPath(import.meta.url)), ".."), "agent-skill.md");
155
+ if (!existsSync(agentSkillPath)) throw new Error("agent-skill.md not found in @timber-js/app package. This may be a development install — try reinstalling.");
156
+ const agentSkill = readFileSync(agentSkillPath, "utf-8");
157
+ const agentsPath = resolve(root, "AGENTS.md");
158
+ const claudePath = resolve(root, "CLAUDE.md");
159
+ const wrote = [];
160
+ if (existsSync(agentsPath)) console.log("[timber] AGENTS.md already exists — skipping (delete it first to regenerate)");
161
+ else {
162
+ writeFileSync(agentsPath, agentSkill);
163
+ wrote.push("AGENTS.md");
164
+ }
165
+ if (existsSync(claudePath)) console.log("[timber] CLAUDE.md already exists — skipping (delete it first to regenerate)");
166
+ else {
167
+ writeFileSync(claudePath, "Read AGENTS.md for project context and conventions.\n");
168
+ wrote.push("CLAUDE.md");
169
+ }
170
+ if (wrote.length > 0) {
171
+ console.log(`[timber] Created ${wrote.join(" and ")} — AI agents can now discover timber.js docs.`);
172
+ console.log("[timber] Full documentation: node_modules/@timber-js/app/docs/");
173
+ } else console.log("[timber] Nothing to do — both files already exist.");
174
+ }
141
175
  async function main() {
142
176
  const parsed = parseArgs(process.argv.slice(2));
143
177
  const options = { config: parsed.config };
@@ -157,6 +191,9 @@ async function main() {
157
191
  case "schema":
158
192
  await runSchema(parsed.rest);
159
193
  break;
194
+ case "init":
195
+ await runInit();
196
+ break;
160
197
  }
161
198
  }
162
199
  if (typeof process !== "undefined" && process.argv[1] && (import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("bin/timber.mjs") || process.argv[1].endsWith("bin/timber"))) main().catch((err) => {
@@ -164,6 +201,6 @@ if (typeof process !== "undefined" && process.argv[1] && (import.meta.url.endsWi
164
201
  process.exit(1);
165
202
  });
166
203
  //#endregion
167
- export { parseArgs, resolvePreviewStrategy, runBuild, runCheck, runDev, runPreview, runSchema };
204
+ export { parseArgs, resolvePreviewStrategy, runBuild, runCheck, runDev, runInit, runPreview, runSchema };
168
205
 
169
206
  //# sourceMappingURL=cli.js.map
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n// timber.js CLI\n//\n// Wraps Vite commands with timber-specific behavior.\n// See design/18-build-system.md §\"CLI\".\n//\n// Commands:\n// timber dev — Start Vite dev server with HMR\n// timber build — Run multi-environment build via createBuilder/buildApp\n// timber preview — Serve the production build\n// timber check — Validate types + routes without building\n\nconst COMMANDS = ['dev', 'build', 'preview', 'check', 'schema'] as const;\ntype Command = (typeof COMMANDS)[number];\n\nexport interface ParsedArgs {\n command: Command;\n config: string | undefined;\n /** Positional arguments after the command (used by `timber schema sync`). */\n rest: string[];\n}\n\nexport interface CommandOptions {\n config?: string;\n}\n\n/**\n * Parse CLI arguments into a structured command + options.\n * Accepts: timber <command> [--config|-c <path>]\n *\n * Unknown flags are an error, not silently ignored — `timber dev --port 4000`\n * starting on the default port would contradict fail-loudly. The CLI surface\n * is intentionally --config-only (design/11-platform.md §CLI); port selection\n * goes through the PORT env var or server.port in vite.config.ts.\n */\nexport function parseArgs(args: string[]): ParsedArgs {\n if (args.length === 0) {\n throw new Error(\n 'No command provided. Usage: timber <dev|build|preview|check|schema> [--config <path>]'\n );\n }\n\n const command = args[0];\n if (!COMMANDS.includes(command as Command)) {\n throw new Error(`Unknown command: ${command}. Available commands: ${COMMANDS.join(', ')}`);\n }\n\n let config: string | undefined;\n const rest: string[] = [];\n for (let i = 1; i < args.length; i++) {\n const arg = args[i];\n if (arg === '--config' || arg === '-c') {\n config = args[++i];\n if (!config) {\n throw new Error('--config requires a path argument');\n }\n } else if (arg.startsWith('-')) {\n const portHint =\n arg === '--port' || arg === '-p'\n ? ' To change the port, set the PORT environment variable (PORT=4000 timber dev) ' +\n 'or server.port in vite.config.ts.'\n : '';\n throw new Error(\n `Unknown option: ${arg}. The timber CLI only accepts --config <path>.${portHint}`\n );\n } else if (command === 'schema') {\n rest.push(arg);\n } else {\n throw new Error(`Unexpected argument: ${arg}. Usage: timber ${command} [--config <path>]`);\n }\n }\n\n return { command: command as Command, config, rest };\n}\n\n// ─── Command Implementations ─────────────────────────────────────────────────\n\n/** @internal Dependency injection for testing. */\nexport interface ViteDeps {\n createServer?: typeof import('vite').createServer;\n createBuilder?: typeof import('vite').createBuilder;\n preview?: typeof import('vite').preview;\n}\n\n/**\n * Start the Vite dev server.\n *\n * The timber plugin binds the port immediately with a holding page\n * (in rootSync's config hook) so browsers see a \"starting...\" page\n * instead of ERR_CONNECTION_REFUSED during initialization.\n *\n * See design/21-dev-server.md, TIM-665.\n */\nexport async function runDev(options: CommandOptions, _deps?: ViteDeps): Promise<void> {\n const createServer = _deps?.createServer ?? (await import('vite')).createServer;\n const server = await createServer({\n configFile: options.config,\n });\n await server.listen();\n server.printUrls();\n}\n\n/**\n * Run the production build using createBuilder + buildApp.\n * Direct build() calls do NOT trigger the RSC plugin's multi-environment\n * pipeline — createBuilder/buildApp is required.\n */\nexport async function runBuild(options: CommandOptions, _deps?: ViteDeps): Promise<void> {\n const createBuilder = _deps?.createBuilder ?? (await import('vite')).createBuilder;\n const builder = await createBuilder({\n configFile: options.config,\n });\n await builder.buildApp();\n}\n\n/**\n * Determine whether to use the adapter's preview or Vite's built-in preview.\n * Exported for testing — the actual runPreview function uses this internally.\n */\nexport function resolvePreviewStrategy(\n adapter: import('./adapters/types').TimberPlatformAdapter | undefined\n): 'adapter' | 'vite' {\n if (adapter && typeof adapter.preview === 'function') {\n return 'adapter';\n }\n return 'vite';\n}\n\n/**\n * Serve the production build for local testing.\n * If the adapter provides a preview() method, it takes priority.\n * Otherwise falls back to Vite's built-in preview server.\n *\n * Uses the same loadTimberConfigFile loader as dev/build — one loader,\n * one set of module semantics. A config file that exists but fails to\n * load throws (naming the file) instead of silently degrading to plain\n * Vite preview. See TIM-1066.\n */\nexport async function runPreview(options: CommandOptions, _deps?: ViteDeps): Promise<void> {\n const { loadTimberConfigFile, resolveBuildDir } = await import('./plugin-context.js');\n\n const root = process.cwd();\n const config = loadTimberConfigFile(root);\n const adapter = config?.adapter as import('./adapters/types').TimberPlatformAdapter | undefined;\n\n if (resolvePreviewStrategy(adapter) === 'adapter') {\n const buildDir = resolveBuildDir(root, config?.buildDir);\n const timberConfig = { output: (config?.output ?? 'server') as 'server' | 'static' };\n await adapter!.preview!(timberConfig, buildDir);\n return;\n }\n\n // Fallback: Vite's built-in preview server\n const preview = _deps?.preview ?? (await import('vite')).preview;\n const server = await preview({\n configFile: options.config,\n });\n server.printUrls();\n}\n\n/**\n * Validate types and routes without producing build output.\n * Runs tsgo --noEmit for type checking.\n */\nexport async function runCheck(options: CommandOptions): Promise<void> {\n const { execFile } = await import('node:child_process');\n\n await new Promise<void>((resolve, reject) => {\n const configArgs = options.config ? ['--project', options.config] : [];\n execFile('tsgo', ['--noEmit', ...configArgs], (err, stdout, stderr) => {\n if (stdout) process.stdout.write(stdout);\n if (stderr) process.stderr.write(stderr);\n if (err) {\n reject(new Error(`Type check failed with exit code ${err.code}`));\n } else {\n resolve();\n }\n });\n });\n}\n\n// ─── Schema Subcommand ───────────────────────────────────────────────────────\n\n/**\n * `timber schema sync` — scan filesystem for dynamic segments, diff against\n * app/schema.ts, and append missing entries with type-appropriate codec defaults.\n *\n * See design/41-global-params.md §Auto-Scaffolding\n */\nexport async function runSchema(args: string[]): Promise<void> {\n const subcommand = args[0];\n\n if (subcommand !== 'sync') {\n throw new Error(\n `Unknown schema subcommand: ${subcommand ?? '(none)'}. Usage: timber schema sync`\n );\n }\n\n const { runSchemaSync, defaultCodecForSegment } = await import('./cli-schema-sync.js');\n const result = runSchemaSync(process.cwd());\n\n if (result.filesystemSegments.length === 0) {\n console.log('[timber] No dynamic segments found in app/.');\n return;\n }\n\n if (result.created) {\n console.log(`[timber] Created app/schema.ts with ${result.addedSegments.length} segment(s):`);\n for (const seg of result.addedSegments) {\n console.log(` + ${seg}: ${defaultCodecForSegment(seg)}`);\n }\n return;\n }\n\n if (result.addedSegments.length === 0 && result.removedSegments.length === 0) {\n console.log('[timber] Schema is up to date — all segments are registered.');\n return;\n }\n\n if (result.addedSegments.length > 0) {\n console.log(`[timber] Added ${result.addedSegments.length} segment(s) to app/schema.ts:`);\n for (const seg of result.addedSegments) {\n console.log(` + ${seg}: ${defaultCodecForSegment(seg)}`);\n }\n }\n\n if (result.removedSegments.length > 0) {\n console.log(\n `[timber] Removed ${result.removedSegments.length} stale segment(s) from app/schema.ts:`\n );\n for (const seg of result.removedSegments) {\n console.log(` - ${seg}`);\n }\n }\n}\n\n// ─── Main Entry Point ────────────────────────────────────────────────────────\n\nasync function main(): Promise<void> {\n const parsed = parseArgs(process.argv.slice(2));\n const options: CommandOptions = { config: parsed.config };\n\n switch (parsed.command) {\n case 'dev':\n await runDev(options);\n break;\n case 'build':\n await runBuild(options);\n break;\n case 'preview':\n await runPreview(options);\n break;\n case 'check':\n await runCheck(options);\n break;\n case 'schema':\n await runSchema(parsed.rest);\n break;\n }\n}\n\n// Run main when executed as a CLI (not imported in tests).\n// The bin shim (bin/timber.mjs) does `import '../dist/cli.js'`, so\n// process.argv[1] points to the shim, not this file. We check both:\n// direct execution AND being imported by the timber bin shim.\nconst isDirectExecution =\n typeof process !== 'undefined' &&\n process.argv[1] &&\n (import.meta.url.endsWith(process.argv[1]) ||\n process.argv[1].endsWith('bin/timber.mjs') ||\n process.argv[1].endsWith('bin/timber'));\n\nif (isDirectExecution) {\n main().catch((err) => {\n console.error(err.message);\n process.exit(1);\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n// timber.js CLI\n//\n// Wraps Vite commands with timber-specific behavior.\n// See design/18-build-system.md §\"CLI\".\n//\n// Commands:\n// timber dev — Start Vite dev server with HMR\n// timber build — Run multi-environment build via createBuilder/buildApp\n// timber preview — Serve the production build\n// timber check — Validate types + routes without building\n\nconst COMMANDS = ['dev', 'build', 'preview', 'check', 'schema', 'init'] as const;\ntype Command = (typeof COMMANDS)[number];\n\nexport interface ParsedArgs {\n command: Command;\n config: string | undefined;\n /** Positional arguments after the command (used by `timber schema sync`). */\n rest: string[];\n}\n\nexport interface CommandOptions {\n config?: string;\n}\n\n/**\n * Parse CLI arguments into a structured command + options.\n * Accepts: timber <command> [--config|-c <path>]\n *\n * Unknown flags are an error, not silently ignored — `timber dev --port 4000`\n * starting on the default port would contradict fail-loudly. The CLI surface\n * is intentionally --config-only (design/11-platform.md §CLI); port selection\n * goes through the PORT env var or server.port in vite.config.ts.\n */\nexport function parseArgs(args: string[]): ParsedArgs {\n if (args.length === 0) {\n throw new Error(\n 'No command provided. Usage: timber <dev|build|preview|check|schema|init> [--config <path>]'\n );\n }\n\n const command = args[0];\n if (!COMMANDS.includes(command as Command)) {\n throw new Error(`Unknown command: ${command}. Available commands: ${COMMANDS.join(', ')}`);\n }\n\n let config: string | undefined;\n const rest: string[] = [];\n for (let i = 1; i < args.length; i++) {\n const arg = args[i];\n if (arg === '--config' || arg === '-c') {\n config = args[++i];\n if (!config) {\n throw new Error('--config requires a path argument');\n }\n } else if (arg.startsWith('-')) {\n const portHint =\n arg === '--port' || arg === '-p'\n ? ' To change the port, set the PORT environment variable (PORT=4000 timber dev) ' +\n 'or server.port in vite.config.ts.'\n : '';\n throw new Error(\n `Unknown option: ${arg}. The timber CLI only accepts --config <path>.${portHint}`\n );\n } else if (command === 'schema' || command === 'init') {\n rest.push(arg);\n } else {\n throw new Error(`Unexpected argument: ${arg}. Usage: timber ${command} [--config <path>]`);\n }\n }\n\n return { command: command as Command, config, rest };\n}\n\n// ─── Command Implementations ─────────────────────────────────────────────────\n\n/** @internal Dependency injection for testing. */\nexport interface ViteDeps {\n createServer?: typeof import('vite').createServer;\n createBuilder?: typeof import('vite').createBuilder;\n preview?: typeof import('vite').preview;\n}\n\n/**\n * Start the Vite dev server.\n *\n * The timber plugin binds the port immediately with a holding page\n * (in rootSync's config hook) so browsers see a \"starting...\" page\n * instead of ERR_CONNECTION_REFUSED during initialization.\n *\n * See design/21-dev-server.md, TIM-665.\n */\nexport async function runDev(options: CommandOptions, _deps?: ViteDeps): Promise<void> {\n const createServer = _deps?.createServer ?? (await import('vite')).createServer;\n const server = await createServer({\n configFile: options.config,\n });\n await server.listen();\n server.printUrls();\n}\n\n/**\n * Run the production build using createBuilder + buildApp.\n * Direct build() calls do NOT trigger the RSC plugin's multi-environment\n * pipeline — createBuilder/buildApp is required.\n */\nexport async function runBuild(options: CommandOptions, _deps?: ViteDeps): Promise<void> {\n const createBuilder = _deps?.createBuilder ?? (await import('vite')).createBuilder;\n const builder = await createBuilder({\n configFile: options.config,\n });\n await builder.buildApp();\n}\n\n/**\n * Determine whether to use the adapter's preview or Vite's built-in preview.\n * Exported for testing — the actual runPreview function uses this internally.\n */\nexport function resolvePreviewStrategy(\n adapter: import('./adapters/types').TimberPlatformAdapter | undefined\n): 'adapter' | 'vite' {\n if (adapter && typeof adapter.preview === 'function') {\n return 'adapter';\n }\n return 'vite';\n}\n\n/**\n * Serve the production build for local testing.\n * If the adapter provides a preview() method, it takes priority.\n * Otherwise falls back to Vite's built-in preview server.\n *\n * Uses the same loadTimberConfigFile loader as dev/build — one loader,\n * one set of module semantics. A config file that exists but fails to\n * load throws (naming the file) instead of silently degrading to plain\n * Vite preview. See TIM-1066.\n */\nexport async function runPreview(options: CommandOptions, _deps?: ViteDeps): Promise<void> {\n const { loadTimberConfigFile, resolveBuildDir } = await import('./plugin-context.js');\n\n const root = process.cwd();\n const config = loadTimberConfigFile(root);\n const adapter = config?.adapter as import('./adapters/types').TimberPlatformAdapter | undefined;\n\n if (resolvePreviewStrategy(adapter) === 'adapter') {\n const buildDir = resolveBuildDir(root, config?.buildDir);\n const timberConfig = { output: (config?.output ?? 'server') as 'server' | 'static' };\n await adapter!.preview!(timberConfig, buildDir);\n return;\n }\n\n // Fallback: Vite's built-in preview server\n const preview = _deps?.preview ?? (await import('vite')).preview;\n const server = await preview({\n configFile: options.config,\n });\n server.printUrls();\n}\n\n/**\n * Validate types and routes without producing build output.\n * Runs tsgo --noEmit for type checking.\n */\nexport async function runCheck(options: CommandOptions): Promise<void> {\n const { execFile } = await import('node:child_process');\n\n await new Promise<void>((resolve, reject) => {\n const configArgs = options.config ? ['--project', options.config] : [];\n execFile('tsgo', ['--noEmit', ...configArgs], (err, stdout, stderr) => {\n if (stdout) process.stdout.write(stdout);\n if (stderr) process.stderr.write(stderr);\n if (err) {\n reject(new Error(`Type check failed with exit code ${err.code}`));\n } else {\n resolve();\n }\n });\n });\n}\n\n// ─── Schema Subcommand ───────────────────────────────────────────────────────\n\n/**\n * `timber schema sync` — scan filesystem for dynamic segments, diff against\n * app/schema.ts, and append missing entries with type-appropriate codec defaults.\n *\n * See design/41-global-params.md §Auto-Scaffolding\n */\nexport async function runSchema(args: string[]): Promise<void> {\n const subcommand = args[0];\n\n if (subcommand !== 'sync') {\n throw new Error(\n `Unknown schema subcommand: ${subcommand ?? '(none)'}. Usage: timber schema sync`\n );\n }\n\n const { runSchemaSync, defaultCodecForSegment } = await import('./cli-schema-sync.js');\n const result = runSchemaSync(process.cwd());\n\n if (result.filesystemSegments.length === 0) {\n console.log('[timber] No dynamic segments found in app/.');\n return;\n }\n\n if (result.created) {\n console.log(`[timber] Created app/schema.ts with ${result.addedSegments.length} segment(s):`);\n for (const seg of result.addedSegments) {\n console.log(` + ${seg}: ${defaultCodecForSegment(seg)}`);\n }\n return;\n }\n\n if (result.addedSegments.length === 0 && result.removedSegments.length === 0) {\n console.log('[timber] Schema is up to date — all segments are registered.');\n return;\n }\n\n if (result.addedSegments.length > 0) {\n console.log(`[timber] Added ${result.addedSegments.length} segment(s) to app/schema.ts:`);\n for (const seg of result.addedSegments) {\n console.log(` + ${seg}: ${defaultCodecForSegment(seg)}`);\n }\n }\n\n if (result.removedSegments.length > 0) {\n console.log(\n `[timber] Removed ${result.removedSegments.length} stale segment(s) from app/schema.ts:`\n );\n for (const seg of result.removedSegments) {\n console.log(` - ${seg}`);\n }\n }\n}\n\n// ─── Init Subcommand ─────────────────────────────────────────────────────────\n\n/**\n * `timber init` — write AGENTS.md (and CLAUDE.md pointer) to the project root\n * so AI coding agents can discover timber.js documentation and conventions.\n *\n * The content is read from the installed @timber-js/app package itself, so it\n * stays in sync with the installed version.\n */\nexport async function runInit(): Promise<void> {\n const { readFileSync, writeFileSync, existsSync } = await import('node:fs');\n const { resolve, dirname } = await import('node:path');\n const { fileURLToPath } = await import('node:url');\n\n const root = process.cwd();\n const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');\n\n const agentSkillPath = resolve(packageRoot, 'agent-skill.md');\n if (!existsSync(agentSkillPath)) {\n throw new Error(\n 'agent-skill.md not found in @timber-js/app package. ' +\n 'This may be a development install — try reinstalling.'\n );\n }\n\n const agentSkill = readFileSync(agentSkillPath, 'utf-8');\n\n const agentsPath = resolve(root, 'AGENTS.md');\n const claudePath = resolve(root, 'CLAUDE.md');\n\n const wrote: string[] = [];\n\n if (existsSync(agentsPath)) {\n console.log('[timber] AGENTS.md already exists — skipping (delete it first to regenerate)');\n } else {\n writeFileSync(agentsPath, agentSkill);\n wrote.push('AGENTS.md');\n }\n\n if (existsSync(claudePath)) {\n console.log('[timber] CLAUDE.md already exists — skipping (delete it first to regenerate)');\n } else {\n writeFileSync(claudePath, 'Read AGENTS.md for project context and conventions.\\n');\n wrote.push('CLAUDE.md');\n }\n\n if (wrote.length > 0) {\n console.log(\n `[timber] Created ${wrote.join(' and ')} — AI agents can now discover timber.js docs.`\n );\n console.log('[timber] Full documentation: node_modules/@timber-js/app/docs/');\n } else {\n console.log('[timber] Nothing to do — both files already exist.');\n }\n}\n\n// ─── Main Entry Point ────────────────────────────────────────────────────────\n\nasync function main(): Promise<void> {\n const parsed = parseArgs(process.argv.slice(2));\n const options: CommandOptions = { config: parsed.config };\n\n switch (parsed.command) {\n case 'dev':\n await runDev(options);\n break;\n case 'build':\n await runBuild(options);\n break;\n case 'preview':\n await runPreview(options);\n break;\n case 'check':\n await runCheck(options);\n break;\n case 'schema':\n await runSchema(parsed.rest);\n break;\n case 'init':\n await runInit();\n break;\n }\n}\n\n// Run main when executed as a CLI (not imported in tests).\n// The bin shim (bin/timber.mjs) does `import '../dist/cli.js'`, so\n// process.argv[1] points to the shim, not this file. We check both:\n// direct execution AND being imported by the timber bin shim.\nconst isDirectExecution =\n typeof process !== 'undefined' &&\n process.argv[1] &&\n (import.meta.url.endsWith(process.argv[1]) ||\n process.argv[1].endsWith('bin/timber.mjs') ||\n process.argv[1].endsWith('bin/timber'));\n\nif (isDirectExecution) {\n main().catch((err) => {\n console.error(err.message);\n process.exit(1);\n });\n}\n"],"mappings":"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"}