@timber-js/app 0.2.0-alpha.153 → 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.
- package/agent-skill.md +344 -0
- package/dist/cli.d.ts +9 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +41 -4
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/server/internal.js +6 -2
- package/dist/server/internal.js.map +1 -1
- package/dist/server/pipeline-metadata.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/sitemap-handler.d.ts.map +1 -1
- package/docs/api/30-api-server.mdx +233 -0
- package/docs/api/31-api-client.mdx +161 -0
- package/docs/api/32-api-cache.mdx +193 -0
- package/docs/api/33-api-search-params.mdx +141 -0
- package/docs/api/34-api-config.mdx +189 -0
- package/docs/api/35-api-typescript.mdx +170 -0
- package/docs/api/36-cli.mdx +143 -0
- package/docs/learn/00-introduction.mdx +81 -0
- package/docs/learn/01-quick-start.mdx +94 -0
- package/docs/learn/02-pages-and-layouts.mdx +138 -0
- package/docs/learn/03-fetching-data.mdx +211 -0
- package/docs/learn/03b-access-control.mdx +130 -0
- package/docs/learn/04-loading-states.mdx +67 -0
- package/docs/learn/04b-the-flush-point.mdx +115 -0
- package/docs/learn/05-typed-params.mdx +275 -0
- package/docs/learn/06-forms-and-actions.mdx +150 -0
- package/docs/learn/08-streaming.mdx +146 -0
- package/docs/learn/09-caching.mdx +121 -0
- package/docs/learn/10-middleware.mdx +156 -0
- package/docs/learn/11-error-handling.mdx +147 -0
- package/docs/learn/12-client-navigation.mdx +168 -0
- package/docs/learn/13-configuration.mdx +117 -0
- package/docs/learn/14-deploying.mdx +206 -0
- package/docs/more/01-advanced-routing.mdx +139 -0
- package/docs/more/02-advanced-forms.mdx +137 -0
- package/docs/more/03-coming-from-nextjs.mdx +186 -0
- package/docs/more/04-metadata-and-fonts.mdx +193 -0
- package/docs/more/04b-mdx.mdx +209 -0
- package/docs/more/05-content-collections.mdx +90 -0
- package/docs/more/06-instrumentation.mdx +188 -0
- package/docs/more/07-security.mdx +123 -0
- package/docs/more/40-why-timber.mdx +50 -0
- package/docs/more/41-timber-vs-nextjs.mdx +81 -0
- package/docs/more/42-timber-vs-others.mdx +68 -0
- package/llms.txt +55 -0
- package/package.json +6 -2
- package/src/cli.ts +62 -3
- package/src/client/browser-entry/action-dispatch.ts +14 -0
- package/src/server/pipeline-metadata.ts +1 -0
- package/src/server/pipeline-phases.ts +4 -1
- package/src/server/sitemap-handler.ts +12 -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,
|
|
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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action-dispatch.d.ts","sourceRoot":"","sources":["../../../src/client/browser-entry/action-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH,wBAAgB,kBAAkB,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"action-dispatch.d.ts","sourceRoot":"","sources":["../../../src/client/browser-entry/action-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH,wBAAgB,kBAAkB,IAAI,IAAI,CA6GzC"}
|
package/dist/server/internal.js
CHANGED
|
@@ -1786,7 +1786,8 @@ async function serveStaticMetadataFile(metaMatch) {
|
|
|
1786
1786
|
const body = await readFile(file.filePath);
|
|
1787
1787
|
const headers = {
|
|
1788
1788
|
"Content-Type": isText ? `${contentType}; charset=utf-8` : contentType,
|
|
1789
|
-
"Content-Length": String(body.byteLength)
|
|
1789
|
+
"Content-Length": String(body.byteLength),
|
|
1790
|
+
"Cache-Control": "public, max-age=14400, must-revalidate"
|
|
1790
1791
|
};
|
|
1791
1792
|
return new Response(body, {
|
|
1792
1793
|
status: 200,
|
|
@@ -2211,7 +2212,10 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
|
|
|
2211
2212
|
else body = String(handlerResult);
|
|
2212
2213
|
return new Response(body, {
|
|
2213
2214
|
status: 200,
|
|
2214
|
-
headers: {
|
|
2215
|
+
headers: {
|
|
2216
|
+
"Content-Type": `${contentType}; charset=utf-8`,
|
|
2217
|
+
"Cache-Control": "public, max-age=14400, must-revalidate"
|
|
2218
|
+
}
|
|
2215
2219
|
});
|
|
2216
2220
|
} catch (error) {
|
|
2217
2221
|
if (error instanceof RedirectSignal) return new Response(null, {
|