ugly-app 0.1.910 → 0.1.911
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 +168 -56
- package/dist/cli/anythingworld.d.ts +31 -0
- package/dist/cli/anythingworld.d.ts.map +1 -0
- package/dist/cli/anythingworld.js +160 -0
- package/dist/cli/anythingworld.js.map +1 -0
- package/dist/cli/index.js +22 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/shared/ProxyOps.d.ts +91 -2
- package/dist/shared/ProxyOps.d.ts.map +1 -1
- package/dist/shared/ProxyOps.js +65 -0
- package/dist/shared/ProxyOps.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/anythingworld.ts +225 -0
- package/src/cli/index.ts +48 -0
- package/src/cli/version.ts +1 -1
- package/src/shared/ProxyOps.ts +66 -0
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# ugly-app
|
|
2
2
|
|
|
3
|
-
A full-stack TypeScript framework for shipping production web apps. Scaffold with
|
|
3
|
+
A full-stack TypeScript framework for shipping production web apps. Scaffold with
|
|
4
|
+
`npx ugly-app init my-app` and get an opinionated Express + React + PostgreSQL stack
|
|
5
|
+
with type-safe RPC over WebSocket and HTTP, real-time document tracking, built-in
|
|
6
|
+
auth, AI generation, storage, workers, and a CLI for every workflow.
|
|
4
7
|
|
|
5
|
-
ugly-app is designed to run against [ugly.bot](https://ugly.bot), which provides
|
|
8
|
+
ugly-app is designed to run against [ugly.bot](https://ugly.bot), which provides
|
|
9
|
+
auth, infra (PostgreSQL, Qdrant, NATS, S3-compatible object storage), AI provider
|
|
10
|
+
keys, and deployment. Your app talks to all of it through the project's dev tunnel
|
|
11
|
+
and its `UGLY_BOT_TOKEN`.
|
|
6
12
|
|
|
7
13
|
## What's included
|
|
8
14
|
|
|
@@ -25,13 +31,16 @@ cd my-app
|
|
|
25
31
|
npm run dev
|
|
26
32
|
```
|
|
27
33
|
|
|
28
|
-
The scaffold gives you a working app at `http://localhost:4321` with todo CRUD, AI
|
|
34
|
+
The scaffold gives you a working app at `http://localhost:4321` with todo CRUD, AI
|
|
35
|
+
chat, file upload, auth demo, collab editing, and other test pages already wired
|
|
36
|
+
up.
|
|
29
37
|
|
|
30
38
|
---
|
|
31
39
|
|
|
32
40
|
## Server — `createApp()` / `AppConfigurator`
|
|
33
41
|
|
|
34
|
-
The single server entry point. Returns an `App` that owns Express, the WebSocket
|
|
42
|
+
The single server entry point. Returns an `App` that owns Express, the WebSocket
|
|
43
|
+
server, the typed DB, and the RPC dispatcher.
|
|
35
44
|
|
|
36
45
|
```ts
|
|
37
46
|
import {
|
|
@@ -76,7 +85,10 @@ function createApp<
|
|
|
76
85
|
): App<CollectionMap<typeof BUILTIN_DEFS & Defs>, RegistryPages<R>>;
|
|
77
86
|
```
|
|
78
87
|
|
|
79
|
-
Passing `pages` in the registry (`{ requests, messages, pages }`) upgrades
|
|
88
|
+
Passing `pages` in the registry (`{ requests, messages, pages }`) upgrades
|
|
89
|
+
`app.pushSend()` to a per-route typed API. `pages` is optional; apps that only call
|
|
90
|
+
`configurator.setPages()` still work — they just get the loose `PageRegistry`
|
|
91
|
+
typing on `pushSend`.
|
|
80
92
|
|
|
81
93
|
### The returned `App`
|
|
82
94
|
|
|
@@ -90,7 +102,11 @@ Passing `pages` in the registry (`{ requests, messages, pages }`) upgrades `app.
|
|
|
90
102
|
| `registerRoutes(fn)` | Mount additional Express routes after creation. |
|
|
91
103
|
| `pushSend({ targetUserId, title, body, page, query, imageUrl?, requireAuth? })` | Send a push whose click-through target is a route from this app's `pages` table. `page` and `query` are type-checked when the registry carries `pages`; the framework mints a `https://ugly.bot/l/<code>` short link so the click-through is always absolute and dock-app-routable. |
|
|
92
104
|
|
|
93
|
-
Framework services start automatically inside `app.start()`: schema drift check,
|
|
105
|
+
Framework services start automatically inside `app.start()`: schema drift check,
|
|
106
|
+
NATS connection + KV buckets, data-proxy connection, event-counter flush, TTL
|
|
107
|
+
cleanup for log tables, console/error capture, and ugly.bot log forwarding.
|
|
108
|
+
Postgres, NATS, storage, and AI clients are loaded **lazily** — a host without
|
|
109
|
+
`DATABASE_URL` / `NATS_URL` / `R2_BUCKET` doesn't pay their startup cost.
|
|
94
110
|
|
|
95
111
|
### `AppConfigurator`
|
|
96
112
|
|
|
@@ -120,7 +136,8 @@ Every method is optional; `setPages` is what mounts the SPA.
|
|
|
120
136
|
|
|
121
137
|
### Handler signatures
|
|
122
138
|
|
|
123
|
-
Handlers are plain async functions — no context object. Access state via captured
|
|
139
|
+
Handlers are plain async functions — no context object. Access state via captured
|
|
140
|
+
imports (`app.db`, `storage`, `pgQuery`, `uglyBotRequest`, etc.).
|
|
124
141
|
|
|
125
142
|
```ts
|
|
126
143
|
// req() — public, userId may be null
|
|
@@ -132,7 +149,9 @@ getMe: async (userId: string, input) => { … }
|
|
|
132
149
|
|
|
133
150
|
### Built-in framework requests
|
|
134
151
|
|
|
135
|
-
`createApp` registers several framework handlers reachable from any client via the
|
|
152
|
+
`createApp` registers several framework handlers reachable from any client via the
|
|
153
|
+
normal RPC pipeline. App-provided handlers with the same name override the
|
|
154
|
+
framework's defaults.
|
|
136
155
|
|
|
137
156
|
| Name | Purpose |
|
|
138
157
|
|------|---------|
|
|
@@ -151,7 +170,8 @@ getMe: async (userId: string, input) => { … }
|
|
|
151
170
|
|
|
152
171
|
## Shared API definitions
|
|
153
172
|
|
|
154
|
-
`shared/` is consumed by both server and client. Keep all Zod schemas, types,
|
|
173
|
+
`shared/` is consumed by both server and client. Keep all Zod schemas, types,
|
|
174
|
+
collections, and route declarations here.
|
|
155
175
|
|
|
156
176
|
### Requests (`shared/api.ts`)
|
|
157
177
|
|
|
@@ -180,7 +200,9 @@ export const requests = defineRequests({
|
|
|
180
200
|
});
|
|
181
201
|
```
|
|
182
202
|
|
|
183
|
-
Every request is reachable as **both** `socket.request(name, input)` (WebSocket)
|
|
203
|
+
Every request is reachable as **both** `socket.request(name, input)` (WebSocket)
|
|
204
|
+
and `POST /api/:name { input }` (HTTP). `z` is re-exported from Zod for
|
|
205
|
+
convenience.
|
|
184
206
|
|
|
185
207
|
### Collections (`shared/collections.ts`)
|
|
186
208
|
|
|
@@ -221,15 +243,21 @@ export const collections = defineCollections({
|
|
|
221
243
|
- `search?: { fields, language? }` — full-text index over the named JSONB paths (Neon: Postgres FTS; D1: SQLite FTS5, ranked by bm25).
|
|
222
244
|
- `vector?: { dimensions, metric?, filterable? }` — ANN index. The vector is supplied out-of-band at write time (`setDoc(c, doc, { vec })`) — never stored in the doc JSON. Query with `getDocs(c, filter, { near })`.
|
|
223
245
|
|
|
224
|
-
All documents extend `DBObject`: `{ _id, version, created, updated }`. Use
|
|
246
|
+
All documents extend `DBObject`: `{ _id, version, created, updated }`. Use
|
|
247
|
+
`dbDefaults()` to stamp `version` / `created` / `updated` on inserts. **Always
|
|
248
|
+
generate `_id` with `nanoid()`** — never `crypto.randomUUID()`, `Date.now()`, or
|
|
249
|
+
`Math.random()` (see `CLAUDE.md`).
|
|
225
250
|
|
|
226
|
-
After schema changes, run `npm run db:schema-gen` then `npm run db:migrate`. The
|
|
251
|
+
After schema changes, run `npm run db:schema-gen` then `npm run db:migrate`. The
|
|
252
|
+
app refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true` only as
|
|
253
|
+
a last resort).
|
|
227
254
|
|
|
228
255
|
---
|
|
229
256
|
|
|
230
257
|
## Routing
|
|
231
258
|
|
|
232
|
-
Route definitions live in `shared/pages.ts`; the client-side router is created by
|
|
259
|
+
Route definitions live in `shared/pages.ts`; the client-side router is created by
|
|
260
|
+
`createRouter({ pages, allPages?, ssrPages? })`.
|
|
233
261
|
|
|
234
262
|
### `definePage` / `definePages` (`shared/pages.ts`)
|
|
235
263
|
|
|
@@ -245,16 +273,21 @@ export const pages = definePages({
|
|
|
245
273
|
export type AppPages = typeof pages;
|
|
246
274
|
```
|
|
247
275
|
|
|
248
|
-
`definePage<Params>(options?)` returns a `PageDef<Params>` — a runtime object
|
|
276
|
+
`definePage<Params>(options?)` returns a `PageDef<Params>` — a runtime object
|
|
277
|
+
carrying `PageMeta` plus a phantom `_params` used only for TypeScript inference.
|
|
278
|
+
Options:
|
|
249
279
|
|
|
250
280
|
- `auth` (default `true`) — protected route. When `isAuthenticated()` returns false on this route, the router synchronously renders the framework's `<AuthRoot>` in place of the page (dispatches on `window.__UGLY_APP_AUTH_MODE__`: Mode A → `<LoginPopup>`, Mode B → `<MagicLinkForm>` plus optional Google button). Apps cannot override this fallback — the framework owns the login UX so no route can accidentally ship as auth-required with no way to log in.
|
|
251
281
|
- `ssr` (default `false`) — server-render the page for SEO. `{ auth: true, ssr: true }` is silently dropped to `ssr: false` with a warning: the SSR document is served from a shared edge cache and must not depend on the viewer.
|
|
252
282
|
- `cacheQuery?: string[]` — query params that participate in the SSR edge-cache key. Anything NOT listed bypasses the cache instead of poisoning it (e.g. `cacheQuery: ['q']` on a search page).
|
|
253
283
|
- `ssrCacheTimeout?: number` — edge cache lifetime in seconds. Default is effectively 1 year (`DEFAULT_SSR_CACHE_TIMEOUT`) because the `buildId` is part of the cache key. Override only for pages that drift between deploys.
|
|
254
284
|
|
|
255
|
-
Path syntax: `:param` matches a single path segment; `*param` is greedy (captures
|
|
285
|
+
Path syntax: `:param` matches a single path segment; `*param` is greedy (captures
|
|
286
|
+
slashes). Query-string params are declared in `Params` but never appear in the
|
|
287
|
+
path template.
|
|
256
288
|
|
|
257
|
-
`definePages<T>(p)` is a pass-through identity — use it to give the registry a
|
|
289
|
+
`definePages<T>(p)` is a pass-through identity — use it to give the registry a
|
|
290
|
+
name.
|
|
258
291
|
|
|
259
292
|
### `createRouter()` (`ugly-app/client`)
|
|
260
293
|
|
|
@@ -285,9 +318,13 @@ export const {
|
|
|
285
318
|
- **`Link`** — typed SPA link bound to this router. Renders a real `<a>` (right-click / cmd-click / SEO keep working) but intercepts a plain left-click and routes through `push` / `replace`. Props: `to`, `params` (both type-checked against `pages`), `replace?`, `children`, plus common anchor attrs (`className`, `style`, `target`, `aria-label`, `data-id`, `onClick`).
|
|
286
319
|
- **`setAllPages(map)`** — register the lazy `PageMap<Pages>` after construction (typically from the browser entry, so the Worker's import graph stays free of every lazy page).
|
|
287
320
|
|
|
288
|
-
A standalone `Link` is also exported from `ugly-app/client` for code that can't
|
|
321
|
+
A standalone `Link` is also exported from `ugly-app/client` for code that can't
|
|
322
|
+
easily reach the typed one; it accepts an optional `router` prop and falls back
|
|
323
|
+
to `<RouterProvider>` context.
|
|
289
324
|
|
|
290
|
-
**Never use a bare `<a href="/route">`** for internal navigation — it triggers a
|
|
325
|
+
**Never use a bare `<a href="/route">`** for internal navigation — it triggers a
|
|
326
|
+
full document reload (white flash + repaint). See the "client navigation" rule in
|
|
327
|
+
`CLAUDE.md`.
|
|
291
328
|
|
|
292
329
|
### Page map — `lazyPage` / `lazyPageLoader`
|
|
293
330
|
|
|
@@ -307,7 +344,10 @@ export const allPages = {
|
|
|
307
344
|
- **`lazyPage(factory)`** — lazy-imports a default-exported `React.ComponentType<Params>`. The page receives route params as props.
|
|
308
345
|
- **`lazyPageLoader(factory)`** — lazy-imports an async loader `(params) => Promise<ReactElement>`. Use when a route needs data fetching before render. The loader file is the chunk boundary, so it can statically import its page component.
|
|
309
346
|
|
|
310
|
-
Both wrappers recover from stale-deploy chunk 404s ("Failed to fetch dynamically
|
|
347
|
+
Both wrappers recover from stale-deploy chunk 404s ("Failed to fetch dynamically
|
|
348
|
+
imported module"): on the first failure they trigger a single
|
|
349
|
+
`window.location.reload()` (guarded by `sessionStorage` so a genuinely broken
|
|
350
|
+
chunk can't loop) to fetch the fresh `index.html` with new chunk hashes.
|
|
311
351
|
|
|
312
352
|
```ts
|
|
313
353
|
// pages/SlowPageLoader.tsx
|
|
@@ -337,11 +377,15 @@ replace('search', { q: 'hello' }); // → /search?q=hello
|
|
|
337
377
|
back(); // browser history back
|
|
338
378
|
```
|
|
339
379
|
|
|
340
|
-
Route names and params are fully typed against `pages`. `push` / `replace` no-op
|
|
380
|
+
Route names and params are fully typed against `pages`. `push` / `replace` no-op
|
|
381
|
+
with a `console.error` when `buildUrl()` produces a URL that doesn't match a
|
|
382
|
+
registered route.
|
|
341
383
|
|
|
342
384
|
### Popups — `openPopup()`
|
|
343
385
|
|
|
344
|
-
`useRouter().openPopup()` is the canonical modal / sheet / menu API. The router
|
|
386
|
+
`useRouter().openPopup()` is the canonical modal / sheet / menu API. The router
|
|
387
|
+
owns the popup layer, drives a spring animation, and stacks popups z-index-
|
|
388
|
+
correctly above every page.
|
|
345
389
|
|
|
346
390
|
```tsx
|
|
347
391
|
const { openPopup } = useRouter();
|
|
@@ -365,13 +409,18 @@ handle.hide(); // dismiss programmatically
|
|
|
365
409
|
- **`transient`** — 20% opacity backdrop, **dismisses** on backdrop click.
|
|
366
410
|
- **`contextMenu`** — same as transient, intended for menus and pickers.
|
|
367
411
|
|
|
368
|
-
`renderLayer` receives `{ content, spring, hide }`: `spring` is a
|
|
412
|
+
`renderLayer` receives `{ content, spring, hide }`: `spring` is a
|
|
413
|
+
`createAnimatedValue()` result driving 0 → 1; `hide` closes the popup. The default
|
|
414
|
+
layer animates open with `easeOut` (250 ms) and closed with `easeIn` (200 ms).
|
|
415
|
+
Popups render as siblings of the router's children (managed inside
|
|
416
|
+
`RouterProvider`), so they stack above every page.
|
|
369
417
|
|
|
370
418
|
---
|
|
371
419
|
|
|
372
420
|
## Client — `bootstrapApp()`
|
|
373
421
|
|
|
374
|
-
The recommended entry point. Handles auth detection, socket creation, background
|
|
422
|
+
The recommended entry point. Handles auth detection, socket creation, background
|
|
423
|
+
account-sync with ugly.bot, and provider wiring.
|
|
375
424
|
|
|
376
425
|
```tsx
|
|
377
426
|
// client/main.tsx
|
|
@@ -403,22 +452,34 @@ bootstrapApp({
|
|
|
403
452
|
| `silentSso?` | Apex-domain apps that use ugly.bot as their auth authority but don't share its cookie. When `true`, a logged-out boot attempts a top-level SSO redirect (once per tab) to adopt an existing ugly.bot session. Leave unset for `*.ugly.bot` subdomains and Mode B apps. |
|
|
404
453
|
| `testMode?` | Skip ugly.bot silent SSO and trust the auth cookie the fixture set. Only honored when `UGLY_APP_TEST_MODE=1`. |
|
|
405
454
|
|
|
406
|
-
`bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server after cookie
|
|
455
|
+
`bootstrapApp` reads `window.__AUTH_TOKEN__` (injected by the server after cookie
|
|
456
|
+
verification). If absent, it renders unauthenticated immediately — the router's
|
|
457
|
+
per-route auth guard surfaces `<AuthRoot>` only when the user opens a protected
|
|
458
|
+
route. If the token is present, it connects the socket, mounts `<AppProvider>`,
|
|
459
|
+
and renders.
|
|
407
460
|
|
|
408
|
-
After render, a hidden iframe silently calls `${UGLY_BOT_URL}/oauth/silent`: if it
|
|
461
|
+
After render, a hidden iframe silently calls `${UGLY_BOT_URL}/oauth/silent`: if it
|
|
462
|
+
returns a fresh code, the cookie is refreshed via `POST /auth/verify`; if the
|
|
463
|
+
returned account differs from the current session, the page reloads onto the live
|
|
464
|
+
account (guarded once per from→to pair to prevent loops). Any
|
|
465
|
+
`?ugly_oauth_code=…` in the URL (redirect-fallback OAuth code from ugly.bot) is
|
|
466
|
+
redeemed at the top of bootstrap before anything renders.
|
|
409
467
|
|
|
410
|
-
If `bootstrapApp` is loaded at `/auth/magic-link/verify` (Mode B), it renders
|
|
468
|
+
If `bootstrapApp` is loaded at `/auth/magic-link/verify` (Mode B), it renders
|
|
469
|
+
`<MagicLinkCallback>` instead of the regular app shell.
|
|
411
470
|
|
|
412
471
|
### `AppProvider` & `useApp()`
|
|
413
472
|
|
|
414
|
-
`bootstrapApp` mounts `<AppProvider>` automatically after socket connect. Use
|
|
473
|
+
`bootstrapApp` mounts `<AppProvider>` automatically after socket connect. Use
|
|
474
|
+
`useApp()` inside any page to access the active user, socket, and app-scoped
|
|
475
|
+
services.
|
|
415
476
|
|
|
416
477
|
```ts
|
|
417
478
|
const {
|
|
418
479
|
userId, // current user id (string)
|
|
419
480
|
user, // UserBase doc
|
|
420
481
|
socket, // AppSocket — typed RPC client
|
|
421
|
-
uglyBotSocket, // UglyBotSocket | null —
|
|
482
|
+
uglyBotSocket, // UglyBotSocket | null — direct platform socket for STT/TTS, etc.
|
|
422
483
|
showPopup, // AppProvider-owned popup layer (see note below)
|
|
423
484
|
hidePopup,
|
|
424
485
|
hideAllPopups,
|
|
@@ -428,9 +489,17 @@ const {
|
|
|
428
489
|
} = useApp();
|
|
429
490
|
```
|
|
430
491
|
|
|
431
|
-
`useApp<TAsyncOptions>()` is generic in the async-options type so apps can pass a
|
|
492
|
+
`useApp<TAsyncOptions>()` is generic in the async-options type so apps can pass a
|
|
493
|
+
custom option through to a custom `loadingOverlay` element (accepted as an
|
|
494
|
+
`AppProvider` prop — the framework `cloneElement`s the overlay with
|
|
495
|
+
`{ label, asyncOptions }` while `runAsync` is pending). `useAppOptional()`
|
|
496
|
+
returns `null` outside the provider; `useLocalizer()` returns a localizer that
|
|
497
|
+
prefers `<StringsProvider>` data, falls back to the `AppProvider` `localizer`
|
|
498
|
+
prop, and finally to identity.
|
|
432
499
|
|
|
433
|
-
Two popup APIs coexist: `useRouter().openPopup()` (spring-animated, dismissible,
|
|
500
|
+
Two popup APIs coexist: `useRouter().openPopup()` (spring-animated, dismissible,
|
|
501
|
+
mode-aware — the default choice) and `useApp().showPopup()` (a flat overlay layer
|
|
502
|
+
for content that must sit *outside* the router transition). Prefer the router's.
|
|
434
503
|
|
|
435
504
|
### Direct socket access
|
|
436
505
|
|
|
@@ -447,20 +516,23 @@ Two popup APIs coexist: `useRouter().openPopup()` (spring-animated, dismissible,
|
|
|
447
516
|
| `connectionState` | `'connecting' \| 'connected' \| 'reconnecting' \| 'disconnected' \| 'idle-disconnected'`. |
|
|
448
517
|
| `disconnect()` | Close the connection. |
|
|
449
518
|
|
|
450
|
-
For pure HTTP (no WebSocket), use `createHttpClient({ requests, token?, baseUrl? })`
|
|
519
|
+
For pure HTTP (no WebSocket), use `createHttpClient({ requests, token?, baseUrl? })`
|
|
520
|
+
from `ugly-app/client`.
|
|
451
521
|
|
|
452
522
|
---
|
|
453
523
|
|
|
454
524
|
## Auth
|
|
455
525
|
|
|
456
|
-
ugly-app uses HttpOnly cookies and server-side JWT injection — no `localStorage`,
|
|
526
|
+
ugly-app uses HttpOnly cookies and server-side JWT injection — no `localStorage`,
|
|
527
|
+
no client-side token handling.
|
|
457
528
|
|
|
458
529
|
Two modes, picked from the `auth` block in the project's `.uglyapp` config:
|
|
459
530
|
|
|
460
531
|
- **Mode A — `mode: 'uglybot'`** (default when no `auth` block is present). Auth is delegated to ugly.bot OAuth. AI / email / push proxies bill the end user's ugly.bot credits.
|
|
461
532
|
- **Mode B — `mode: 'self'`**. The app issues its own sessions via magic-link email and/or Google OAuth. AI / email / push proxies bill the developer's ugly.bot account using the project's `AI_PROXY_TOKEN`.
|
|
462
533
|
|
|
463
|
-
`window.__UGLY_APP_AUTH_MODE__` is injected into every page so client code (incl.
|
|
534
|
+
`window.__UGLY_APP_AUTH_MODE__` is injected into every page so client code (incl.
|
|
535
|
+
`<AuthRoot>`) can branch correctly.
|
|
464
536
|
|
|
465
537
|
### Mode A — ugly.bot OAuth (default)
|
|
466
538
|
|
|
@@ -472,7 +544,9 @@ Two modes, picked from the `auth` block in the project's `.uglyapp` config:
|
|
|
472
544
|
|
|
473
545
|
### Mode B — self-issued sessions
|
|
474
546
|
|
|
475
|
-
When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link
|
|
547
|
+
When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link
|
|
548
|
+
provider as primary; if `providers.google.clientId` is configured it adds Google
|
|
549
|
+
as an extra provider on the same router.
|
|
476
550
|
|
|
477
551
|
- `<AuthRoot>` renders `<MagicLinkForm>` (plus the Google button when configured) instead of `<LoginPopup>`.
|
|
478
552
|
- Background ugly.bot silent SSO is a no-op.
|
|
@@ -482,7 +556,9 @@ When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link pro
|
|
|
482
556
|
|
|
483
557
|
### Token-in-URL embed
|
|
484
558
|
|
|
485
|
-
Any GET request with `?token=<JWT>` will, if the token verifies, set the cookie
|
|
559
|
+
Any GET request with `?token=<JWT>` will, if the token verifies, set the cookie
|
|
560
|
+
and 302-redirect to the same URL without the token parameter — useful for
|
|
561
|
+
embedding any page in an iframe.
|
|
486
562
|
|
|
487
563
|
### Built-in routes
|
|
488
564
|
|
|
@@ -515,7 +591,9 @@ configurator.setAuth({
|
|
|
515
591
|
|
|
516
592
|
## Database — `TypedDB`
|
|
517
593
|
|
|
518
|
-
Access via `app.db`. All methods accept a `CollectionDef` (from
|
|
594
|
+
Access via `app.db`. All methods accept a `CollectionDef` (from
|
|
595
|
+
`defineCollections()`) or, on the `raw*` / `getQuery*` variants, a plain
|
|
596
|
+
collection name string.
|
|
519
597
|
|
|
520
598
|
### Writing
|
|
521
599
|
|
|
@@ -540,7 +618,10 @@ await db.batch(async () => {
|
|
|
540
618
|
});
|
|
541
619
|
```
|
|
542
620
|
|
|
543
|
-
Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`, `$set`. All
|
|
621
|
+
Supported update operators: `$inc`, `$addToSet`, `$pull`, `$unset`, `$set`. All
|
|
622
|
+
keys are dot-notation, fully typed against the collection's schema. Partial
|
|
623
|
+
updates go through optimistic concurrency to prevent lost updates on concurrent
|
|
624
|
+
writers.
|
|
544
625
|
|
|
545
626
|
### Reading
|
|
546
627
|
|
|
@@ -574,7 +655,8 @@ await db.deleteWhere(collections.note, { userId }); // typed bulk delete
|
|
|
574
655
|
await db.deleteQuery(collections.note, { userId }); // legacy untyped bulk delete
|
|
575
656
|
```
|
|
576
657
|
|
|
577
|
-
Pass `deleteHandlers` as the 5th argument to `createApp` to run per-collection
|
|
658
|
+
Pass `deleteHandlers` as the 5th argument to `createApp` to run per-collection
|
|
659
|
+
`onDelete` callbacks.
|
|
578
660
|
|
|
579
661
|
### Search
|
|
580
662
|
|
|
@@ -623,7 +705,9 @@ Imports available from `ugly-app`:
|
|
|
623
705
|
|
|
624
706
|
## AI
|
|
625
707
|
|
|
626
|
-
AI calls are proxied through ugly.bot — your app never holds a provider key. Pass
|
|
708
|
+
AI calls are proxied through ugly.bot — your app never holds a provider key. Pass
|
|
709
|
+
`UGLY_BOT_TOKEN` in the environment and the framework handles routing, balance
|
|
710
|
+
tracking, retries, and per-user billing.
|
|
627
711
|
|
|
628
712
|
### Server-side text generation
|
|
629
713
|
|
|
@@ -645,7 +729,9 @@ const { message } = await uglyBotRequest<{ message: { content: string } }>('text
|
|
|
645
729
|
});
|
|
646
730
|
```
|
|
647
731
|
|
|
648
|
-
Available models are exposed via `textGenModels` / `textGenModelData` from
|
|
732
|
+
Available models are exposed via `textGenModels` / `textGenModelData` from
|
|
733
|
+
`ugly-app` — the platform supports Claude, GPT, Gemini, Together, Groq,
|
|
734
|
+
Fireworks, Kimi, and Kie families.
|
|
649
735
|
|
|
650
736
|
### Server-side image generation
|
|
651
737
|
|
|
@@ -656,7 +742,8 @@ const imageGen = createImageGen(userId);
|
|
|
656
742
|
const url = await imageGen.generate('A red panda eating noodles', { model: 'flux_schnell' });
|
|
657
743
|
```
|
|
658
744
|
|
|
659
|
-
`imageGenModels` / `imageGenModelData` enumerate available models (Together FLUX,
|
|
745
|
+
`imageGenModels` / `imageGenModelData` enumerate available models (Together FLUX,
|
|
746
|
+
FAL, Google Imagen, Wavespeed, Kie Kolors).
|
|
660
747
|
|
|
661
748
|
### Embeddings
|
|
662
749
|
|
|
@@ -681,7 +768,8 @@ await search.enrichNews({ query: 'topic' });
|
|
|
681
768
|
|
|
682
769
|
### Client-side AI calls
|
|
683
770
|
|
|
684
|
-
Calls from React components go through the framework RPC pipeline — no token
|
|
771
|
+
Calls from React components go through the framework RPC pipeline — no token
|
|
772
|
+
plumbing in the browser:
|
|
685
773
|
|
|
686
774
|
```ts
|
|
687
775
|
import { callTextGen, callJsonGen, callImageGen } from 'ugly-app/client';
|
|
@@ -693,7 +781,8 @@ const image = await callImageGen({ prompt: 'a corgi astronaut', model: 'flux_sch
|
|
|
693
781
|
|
|
694
782
|
### STT / TTS
|
|
695
783
|
|
|
696
|
-
Speech goes **directly** from the browser to ugly.bot — never proxied through
|
|
784
|
+
Speech goes **directly** from the browser to ugly.bot — never proxied through
|
|
785
|
+
your app server (see the "STT/TTS routes to ugly.bot" rule in `CLAUDE.md`).
|
|
697
786
|
|
|
698
787
|
```ts
|
|
699
788
|
import { useSTT, useTTS, AudioPlayer, AudioRecorder } from 'ugly-app/client';
|
|
@@ -702,7 +791,8 @@ const { start, stop, transcript, isListening } = useSTT(uglyBotSocket, options);
|
|
|
702
791
|
const { speak, stop: stopTTS } = useTTS(uglyBotSocket);
|
|
703
792
|
```
|
|
704
793
|
|
|
705
|
-
`uglyBotSocket` is the `UglyBotSocket` on `useApp()`; it opens a direct WebSocket
|
|
794
|
+
`uglyBotSocket` is the `UglyBotSocket` on `useApp()`; it opens a direct WebSocket
|
|
795
|
+
to the platform.
|
|
706
796
|
|
|
707
797
|
---
|
|
708
798
|
|
|
@@ -726,11 +816,15 @@ const { uploadUrl, resultUrl } = await storage.presignedPut('temp', key);
|
|
|
726
816
|
await storage.delete('public', destKey);
|
|
727
817
|
```
|
|
728
818
|
|
|
729
|
-
Client-side, use `socket.uploadFile(file, key)` — it requests a presigned URL via
|
|
819
|
+
Client-side, use `socket.uploadFile(file, key)` — it requests a presigned URL via
|
|
820
|
+
the built-in `uploadUrl` framework request and streams the upload. In dev,
|
|
821
|
+
uploads go through a same-origin `/_s3` proxy to avoid CORS with local MinIO.
|
|
730
822
|
|
|
731
|
-
`STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for per-environment
|
|
823
|
+
`STORAGE_KEY_PREFIX` (env) prefixes all keys — useful for per-environment
|
|
824
|
+
isolation.
|
|
732
825
|
|
|
733
|
-
On Cloudflare Workers, storage uses the R2 binding directly; presigned PUTs are
|
|
826
|
+
On Cloudflare Workers, storage uses the R2 binding directly; presigned PUTs are
|
|
827
|
+
not exposed (browser uploads must go through a Worker endpoint).
|
|
734
828
|
|
|
735
829
|
---
|
|
736
830
|
|
|
@@ -762,9 +856,15 @@ const cronHandlers: WorkerHandlers<typeof cronTasks> = {
|
|
|
762
856
|
configurator.setWorkers(cronTasks, cronHandlers);
|
|
763
857
|
```
|
|
764
858
|
|
|
765
|
-
Each worker can have `inputSchema`, `outputSchema`, `schedule`, `timeout`,
|
|
859
|
+
Each worker can have `inputSchema`, `outputSchema`, `schedule`, `timeout`,
|
|
860
|
+
`description`. Workers without a schedule are still invocable via
|
|
861
|
+
`POST /_workers/run` (auth: localhost in dev, `Authorization: Bearer $CRON_SECRET`
|
|
862
|
+
in prod). Scheduled workers also appear in `/_cron/manifest` for the deploy
|
|
863
|
+
orchestrator.
|
|
766
864
|
|
|
767
|
-
On the Workers adapter, scheduled workers dispatch through Cloudflare Cron
|
|
865
|
+
On the Workers adapter, scheduled workers dispatch through Cloudflare Cron
|
|
866
|
+
Triggers and durable queueing goes through Cloudflare Queues. On the Node
|
|
867
|
+
adapter, `enqueueWorker` runs the handler inline (in-process queues only).
|
|
768
868
|
|
|
769
869
|
---
|
|
770
870
|
|
|
@@ -779,7 +879,9 @@ configurator.setStrings({
|
|
|
779
879
|
});
|
|
780
880
|
```
|
|
781
881
|
|
|
782
|
-
The framework injects `window.__LANG__`, `window.__STRINGS_VERSION__`, and
|
|
882
|
+
The framework injects `window.__LANG__`, `window.__STRINGS_VERSION__`, and
|
|
883
|
+
`window.__CRITICAL_STRINGS__` into SSR HTML. Use `useLocalizer()` /
|
|
884
|
+
`useStrings()` / `useLang()` / `useChangeLanguage()` on the client.
|
|
783
885
|
|
|
784
886
|
---
|
|
785
887
|
|
|
@@ -804,7 +906,9 @@ export const experiments: Experiment[] = [
|
|
|
804
906
|
configurator.setExperiments(experiments);
|
|
805
907
|
```
|
|
806
908
|
|
|
807
|
-
Bucketing is deterministic: `hash(experimentId + userId)` (or `sessionId` for
|
|
909
|
+
Bucketing is deterministic: `hash(experimentId + userId)` (or `sessionId` for
|
|
910
|
+
unauthenticated users). The framework's `initSession` / `captureEvent` requests
|
|
911
|
+
automatically tag events with the user's branch assignments.
|
|
808
912
|
|
|
809
913
|
---
|
|
810
914
|
|
|
@@ -859,7 +963,10 @@ The same developer source compiles for two runtimes via `src/server/adapter/`:
|
|
|
859
963
|
- **Adapter A — Node + TCP** (`src/server/adapter/node/`): default for `npm run dev` and any non-Workers deploy. Wraps `pg.Pool`, `nats.js`, AWS S3 SDK.
|
|
860
964
|
- **Adapter B — Cloudflare Workers** (`src/server/adapter/workers/`): used when the Studio publish flow deploys to Cloudflare. Hono router + Durable Objects (one `CollectionDO` per project-collection, plus a `SessionDO` per user WS) + `@neondatabase/serverless` HTTP driver + R2 binding + Cloudflare Cron Triggers + Cloudflare Queues.
|
|
861
965
|
|
|
862
|
-
Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`,
|
|
966
|
+
Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`,
|
|
967
|
+
`setCronTasks`, `setWorkers`) don't change between adapters. Local Workers dev
|
|
968
|
+
boots via `npm run dev:workers`, which starts a small Node HTTP proxy speaking
|
|
969
|
+
Neon's wire format so the Worker can talk to a real Postgres.
|
|
863
970
|
|
|
864
971
|
---
|
|
865
972
|
|
|
@@ -888,7 +995,8 @@ Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`, `
|
|
|
888
995
|
| `NEON_PROXY_URL` | Set by `dev:workers` — Neon-wire endpoint the local Neon HTTP driver dials. |
|
|
889
996
|
| `CLOUDFLARE_WORKERS` | Set to `1` inside the Workers adapter for runtime detection. |
|
|
890
997
|
|
|
891
|
-
Browser-visible variables must be prefixed `VITE_` and consumed via
|
|
998
|
+
Browser-visible variables must be prefixed `VITE_` and consumed via
|
|
999
|
+
`import.meta.env.VITE_*`.
|
|
892
1000
|
|
|
893
1001
|
---
|
|
894
1002
|
|
|
@@ -913,7 +1021,8 @@ Browser-visible variables must be prefixed `VITE_` and consumed via `import.meta
|
|
|
913
1021
|
| `ugly-app feedback:dev` / `feedback:prod` | Query user feedback. |
|
|
914
1022
|
| `ugly-app feedback:submit` / `feedback:resolve` | Manage feedback (run with `--help` for flags). |
|
|
915
1023
|
|
|
916
|
-
Inside a scaffolded project, the same commands are available via `npm run …`
|
|
1024
|
+
Inside a scaffolded project, the same commands are available via `npm run …`
|
|
1025
|
+
scripts — see `templates/CLAUDE.md`.
|
|
917
1026
|
|
|
918
1027
|
---
|
|
919
1028
|
|
|
@@ -926,10 +1035,13 @@ Schema changes must be deliberate:
|
|
|
926
1035
|
3. Replace every `REPLACE_ME` with the correct migration logic.
|
|
927
1036
|
4. Run `npm run db:migrate`.
|
|
928
1037
|
|
|
929
|
-
The framework refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true`
|
|
1038
|
+
The framework refuses to start when drift is detected (set `SCHEMA_CHECK_SKIP=true`
|
|
1039
|
+
only as a temporary escape hatch).
|
|
930
1040
|
|
|
931
1041
|
---
|
|
932
1042
|
|
|
933
1043
|
## Tech stack
|
|
934
1044
|
|
|
935
|
-
Node.js · TypeScript · Express · React 19 · Vite · PostgreSQL (JSONB) · Qdrant ·
|
|
1045
|
+
Node.js · TypeScript · Express · React 19 · Vite · PostgreSQL (JSONB) · Qdrant ·
|
|
1046
|
+
NATS · S3-compatible storage · Zod · JWT (jose) · Cloudflare Workers + Durable
|
|
1047
|
+
Objects (Adapter B) · ugly.bot platform
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anything World 3D pipeline via ugly.bot's `aw*` ops. Rigs and animates ANIMALS — the one thing the
|
|
3
|
+
* other two providers cannot do:
|
|
4
|
+
* find <query> search the 3D library (FREE)
|
|
5
|
+
* models every model this account has processed (FREE)
|
|
6
|
+
* status <modelId> poll a job, and print its clips (FREE)
|
|
7
|
+
* animate <file...> upload → rig + animate (~5 credits, ~$1.25)
|
|
8
|
+
* rig <file...> upload → rig only (~5 credits)
|
|
9
|
+
*
|
|
10
|
+
* Measured 2026-07-25 on real four-legged bodies: Meshy returns a 24-joint BIPED (…LeftForeArm,
|
|
11
|
+
* LeftHand) with a clip called `walking_man`, and Tripo FAILS the task on rig_type 'quadruped'.
|
|
12
|
+
* Anything World's library reports `quadruped`, `quadruped_ungulate`, `winged_flyer` and `hopper`,
|
|
13
|
+
* carrying idle + walk + RUN for walkers and fly + glide for flyers.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ TWO BUDGETS. `find` / `models` / `status` are library calls against a free monthly allowance;
|
|
16
|
+
* `animate` / `rig` spend processing credits. Verify and poll on the free side — never by
|
|
17
|
+
* re-submitting. Uploads are multipart, so a local GLB goes straight in with no public hosting.
|
|
18
|
+
*/
|
|
19
|
+
interface AwOpts {
|
|
20
|
+
name?: string;
|
|
21
|
+
type?: string;
|
|
22
|
+
output?: string;
|
|
23
|
+
symmetric?: string;
|
|
24
|
+
autoRotate?: string;
|
|
25
|
+
stage?: string;
|
|
26
|
+
generated?: boolean;
|
|
27
|
+
shareForTraining?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export declare function runAnythingWorld(op: string, args: string[], opts: AwOpts): Promise<void>;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=anythingworld.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anythingworld.d.ts","sourceRoot":"","sources":["../../src/cli/anythingworld.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;;;;;GAiBG;AACH,UAAU,MAAM;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAgGD,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAwFf"}
|