ugly-app 0.1.909 → 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 +171 -58
- 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 +97 -2
- package/dist/shared/ProxyOps.d.ts.map +1 -1
- package/dist/shared/ProxyOps.js +71 -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 +72 -0
package/README.md
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
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
|
|
|
9
15
|
- **Server** — Express + WebSocket with type-safe RPC and Zod validation.
|
|
10
16
|
- **Client** — React 19 + Vite with typed routing, lazy pages, animated transitions, and a router-owned popup layer.
|
|
11
|
-
- **Database** — `TypedDB` over PostgreSQL (JSONB) or Cloudflare D1, with full-text
|
|
17
|
+
- **Database** — `TypedDB` over PostgreSQL (JSONB) or Cloudflare D1, with full-text and vector search.
|
|
12
18
|
- **Auth** — HttpOnly cookies + JWT. ugly.bot OAuth by default; magic-link / Google in `mode: 'self'`.
|
|
13
19
|
- **AI** — Text, JSON, image, embeddings, web search — proxied through ugly.bot.
|
|
14
20
|
- **Realtime** — NATS pub/sub and doc change subscriptions (`trackDoc` / `trackDocs`).
|
|
@@ -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
|
-
## Server — `createApp()`
|
|
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
|
|------|---------|
|
|
@@ -145,12 +164,14 @@ getMe: async (userId: string, input) => { … }
|
|
|
145
164
|
| `feedbackReportCreateNoAuth` / `errorLogCaptureNoAuth` / `perfSnapshotCaptureNoAuth` | Same-origin, public endpoints used by browser telemetry to write into the project's Postgres. |
|
|
146
165
|
| `submitFeedbackBot` / `feedbackReportResolve` | Bot-persona feedback submission; admin resolve/decline. |
|
|
147
166
|
| `adminCreateTestUser` / `adminListTestUsers` / `adminDeleteTestUser` | Admin-only synthetic-user management. Gated by `setIsAdmin()` (or `MAINTAIN_BOT_USER_ID`). |
|
|
167
|
+
| `projectPlanList` / `projectPlanCreate` / `projectPlanUpdate` / `projectPlanDelete` | Project plan CRUD used by Studio. |
|
|
148
168
|
|
|
149
169
|
---
|
|
150
170
|
|
|
151
171
|
## Shared API definitions
|
|
152
172
|
|
|
153
|
-
`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.
|
|
154
175
|
|
|
155
176
|
### Requests (`shared/api.ts`)
|
|
156
177
|
|
|
@@ -179,7 +200,9 @@ export const requests = defineRequests({
|
|
|
179
200
|
});
|
|
180
201
|
```
|
|
181
202
|
|
|
182
|
-
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.
|
|
183
206
|
|
|
184
207
|
### Collections (`shared/collections.ts`)
|
|
185
208
|
|
|
@@ -220,15 +243,21 @@ export const collections = defineCollections({
|
|
|
220
243
|
- `search?: { fields, language? }` — full-text index over the named JSONB paths (Neon: Postgres FTS; D1: SQLite FTS5, ranked by bm25).
|
|
221
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 })`.
|
|
222
245
|
|
|
223
|
-
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`).
|
|
224
250
|
|
|
225
|
-
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).
|
|
226
254
|
|
|
227
255
|
---
|
|
228
256
|
|
|
229
257
|
## Routing
|
|
230
258
|
|
|
231
|
-
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? })`.
|
|
232
261
|
|
|
233
262
|
### `definePage` / `definePages` (`shared/pages.ts`)
|
|
234
263
|
|
|
@@ -244,16 +273,21 @@ export const pages = definePages({
|
|
|
244
273
|
export type AppPages = typeof pages;
|
|
245
274
|
```
|
|
246
275
|
|
|
247
|
-
`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:
|
|
248
279
|
|
|
249
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.
|
|
250
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.
|
|
251
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).
|
|
252
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.
|
|
253
284
|
|
|
254
|
-
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.
|
|
255
288
|
|
|
256
|
-
`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.
|
|
257
291
|
|
|
258
292
|
### `createRouter()` (`ugly-app/client`)
|
|
259
293
|
|
|
@@ -284,9 +318,13 @@ export const {
|
|
|
284
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`).
|
|
285
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).
|
|
286
320
|
|
|
287
|
-
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.
|
|
288
324
|
|
|
289
|
-
**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`.
|
|
290
328
|
|
|
291
329
|
### Page map — `lazyPage` / `lazyPageLoader`
|
|
292
330
|
|
|
@@ -306,7 +344,10 @@ export const allPages = {
|
|
|
306
344
|
- **`lazyPage(factory)`** — lazy-imports a default-exported `React.ComponentType<Params>`. The page receives route params as props.
|
|
307
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.
|
|
308
346
|
|
|
309
|
-
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.
|
|
310
351
|
|
|
311
352
|
```ts
|
|
312
353
|
// pages/SlowPageLoader.tsx
|
|
@@ -336,11 +377,15 @@ replace('search', { q: 'hello' }); // → /search?q=hello
|
|
|
336
377
|
back(); // browser history back
|
|
337
378
|
```
|
|
338
379
|
|
|
339
|
-
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.
|
|
340
383
|
|
|
341
384
|
### Popups — `openPopup()`
|
|
342
385
|
|
|
343
|
-
`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.
|
|
344
389
|
|
|
345
390
|
```tsx
|
|
346
391
|
const { openPopup } = useRouter();
|
|
@@ -364,13 +409,18 @@ handle.hide(); // dismiss programmatically
|
|
|
364
409
|
- **`transient`** — 20% opacity backdrop, **dismisses** on backdrop click.
|
|
365
410
|
- **`contextMenu`** — same as transient, intended for menus and pickers.
|
|
366
411
|
|
|
367
|
-
`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.
|
|
368
417
|
|
|
369
418
|
---
|
|
370
419
|
|
|
371
420
|
## Client — `bootstrapApp()`
|
|
372
421
|
|
|
373
|
-
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.
|
|
374
424
|
|
|
375
425
|
```tsx
|
|
376
426
|
// client/main.tsx
|
|
@@ -402,22 +452,34 @@ bootstrapApp({
|
|
|
402
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. |
|
|
403
453
|
| `testMode?` | Skip ugly.bot silent SSO and trust the auth cookie the fixture set. Only honored when `UGLY_APP_TEST_MODE=1`. |
|
|
404
454
|
|
|
405
|
-
`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.
|
|
406
460
|
|
|
407
|
-
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.
|
|
408
467
|
|
|
409
|
-
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.
|
|
410
470
|
|
|
411
471
|
### `AppProvider` & `useApp()`
|
|
412
472
|
|
|
413
|
-
`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.
|
|
414
476
|
|
|
415
477
|
```ts
|
|
416
478
|
const {
|
|
417
479
|
userId, // current user id (string)
|
|
418
480
|
user, // UserBase doc
|
|
419
481
|
socket, // AppSocket — typed RPC client
|
|
420
|
-
uglyBotSocket, // UglyBotSocket | null —
|
|
482
|
+
uglyBotSocket, // UglyBotSocket | null — direct platform socket for STT/TTS, etc.
|
|
421
483
|
showPopup, // AppProvider-owned popup layer (see note below)
|
|
422
484
|
hidePopup,
|
|
423
485
|
hideAllPopups,
|
|
@@ -427,9 +489,17 @@ const {
|
|
|
427
489
|
} = useApp();
|
|
428
490
|
```
|
|
429
491
|
|
|
430
|
-
`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.
|
|
431
499
|
|
|
432
|
-
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.
|
|
433
503
|
|
|
434
504
|
### Direct socket access
|
|
435
505
|
|
|
@@ -446,20 +516,23 @@ Two popup APIs coexist: `useRouter().openPopup()` (spring-animated, dismissible,
|
|
|
446
516
|
| `connectionState` | `'connecting' \| 'connected' \| 'reconnecting' \| 'disconnected' \| 'idle-disconnected'`. |
|
|
447
517
|
| `disconnect()` | Close the connection. |
|
|
448
518
|
|
|
449
|
-
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`.
|
|
450
521
|
|
|
451
522
|
---
|
|
452
523
|
|
|
453
524
|
## Auth
|
|
454
525
|
|
|
455
|
-
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.
|
|
456
528
|
|
|
457
529
|
Two modes, picked from the `auth` block in the project's `.uglyapp` config:
|
|
458
530
|
|
|
459
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.
|
|
460
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`.
|
|
461
533
|
|
|
462
|
-
`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.
|
|
463
536
|
|
|
464
537
|
### Mode A — ugly.bot OAuth (default)
|
|
465
538
|
|
|
@@ -471,7 +544,9 @@ Two modes, picked from the `auth` block in the project's `.uglyapp` config:
|
|
|
471
544
|
|
|
472
545
|
### Mode B — self-issued sessions
|
|
473
546
|
|
|
474
|
-
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.
|
|
475
550
|
|
|
476
551
|
- `<AuthRoot>` renders `<MagicLinkForm>` (plus the Google button when configured) instead of `<LoginPopup>`.
|
|
477
552
|
- Background ugly.bot silent SSO is a no-op.
|
|
@@ -481,7 +556,9 @@ When `.uglyapp` sets `auth.mode: 'self'`, `buildAuth()` wires the magic-link pro
|
|
|
481
556
|
|
|
482
557
|
### Token-in-URL embed
|
|
483
558
|
|
|
484
|
-
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.
|
|
485
562
|
|
|
486
563
|
### Built-in routes
|
|
487
564
|
|
|
@@ -514,7 +591,9 @@ configurator.setAuth({
|
|
|
514
591
|
|
|
515
592
|
## Database — `TypedDB`
|
|
516
593
|
|
|
517
|
-
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.
|
|
518
597
|
|
|
519
598
|
### Writing
|
|
520
599
|
|
|
@@ -539,7 +618,10 @@ await db.batch(async () => {
|
|
|
539
618
|
});
|
|
540
619
|
```
|
|
541
620
|
|
|
542
|
-
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.
|
|
543
625
|
|
|
544
626
|
### Reading
|
|
545
627
|
|
|
@@ -573,7 +655,8 @@ await db.deleteWhere(collections.note, { userId }); // typed bulk delete
|
|
|
573
655
|
await db.deleteQuery(collections.note, { userId }); // legacy untyped bulk delete
|
|
574
656
|
```
|
|
575
657
|
|
|
576
|
-
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.
|
|
577
660
|
|
|
578
661
|
### Search
|
|
579
662
|
|
|
@@ -622,7 +705,9 @@ Imports available from `ugly-app`:
|
|
|
622
705
|
|
|
623
706
|
## AI
|
|
624
707
|
|
|
625
|
-
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.
|
|
626
711
|
|
|
627
712
|
### Server-side text generation
|
|
628
713
|
|
|
@@ -644,7 +729,9 @@ const { message } = await uglyBotRequest<{ message: { content: string } }>('text
|
|
|
644
729
|
});
|
|
645
730
|
```
|
|
646
731
|
|
|
647
|
-
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.
|
|
648
735
|
|
|
649
736
|
### Server-side image generation
|
|
650
737
|
|
|
@@ -655,7 +742,8 @@ const imageGen = createImageGen(userId);
|
|
|
655
742
|
const url = await imageGen.generate('A red panda eating noodles', { model: 'flux_schnell' });
|
|
656
743
|
```
|
|
657
744
|
|
|
658
|
-
`imageGenModels` / `imageGenModelData` enumerate available models (Together FLUX,
|
|
745
|
+
`imageGenModels` / `imageGenModelData` enumerate available models (Together FLUX,
|
|
746
|
+
FAL, Google Imagen, Wavespeed, Kie Kolors).
|
|
659
747
|
|
|
660
748
|
### Embeddings
|
|
661
749
|
|
|
@@ -680,7 +768,8 @@ await search.enrichNews({ query: 'topic' });
|
|
|
680
768
|
|
|
681
769
|
### Client-side AI calls
|
|
682
770
|
|
|
683
|
-
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:
|
|
684
773
|
|
|
685
774
|
```ts
|
|
686
775
|
import { callTextGen, callJsonGen, callImageGen } from 'ugly-app/client';
|
|
@@ -692,7 +781,8 @@ const image = await callImageGen({ prompt: 'a corgi astronaut', model: 'flux_sch
|
|
|
692
781
|
|
|
693
782
|
### STT / TTS
|
|
694
783
|
|
|
695
|
-
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`).
|
|
696
786
|
|
|
697
787
|
```ts
|
|
698
788
|
import { useSTT, useTTS, AudioPlayer, AudioRecorder } from 'ugly-app/client';
|
|
@@ -701,7 +791,8 @@ const { start, stop, transcript, isListening } = useSTT(uglyBotSocket, options);
|
|
|
701
791
|
const { speak, stop: stopTTS } = useTTS(uglyBotSocket);
|
|
702
792
|
```
|
|
703
793
|
|
|
704
|
-
`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.
|
|
705
796
|
|
|
706
797
|
---
|
|
707
798
|
|
|
@@ -725,11 +816,15 @@ const { uploadUrl, resultUrl } = await storage.presignedPut('temp', key);
|
|
|
725
816
|
await storage.delete('public', destKey);
|
|
726
817
|
```
|
|
727
818
|
|
|
728
|
-
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.
|
|
729
822
|
|
|
730
|
-
`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.
|
|
731
825
|
|
|
732
|
-
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).
|
|
733
828
|
|
|
734
829
|
---
|
|
735
830
|
|
|
@@ -761,9 +856,15 @@ const cronHandlers: WorkerHandlers<typeof cronTasks> = {
|
|
|
761
856
|
configurator.setWorkers(cronTasks, cronHandlers);
|
|
762
857
|
```
|
|
763
858
|
|
|
764
|
-
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.
|
|
765
864
|
|
|
766
|
-
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).
|
|
767
868
|
|
|
768
869
|
---
|
|
769
870
|
|
|
@@ -778,7 +879,9 @@ configurator.setStrings({
|
|
|
778
879
|
});
|
|
779
880
|
```
|
|
780
881
|
|
|
781
|
-
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.
|
|
782
885
|
|
|
783
886
|
---
|
|
784
887
|
|
|
@@ -803,7 +906,9 @@ export const experiments: Experiment[] = [
|
|
|
803
906
|
configurator.setExperiments(experiments);
|
|
804
907
|
```
|
|
805
908
|
|
|
806
|
-
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.
|
|
807
912
|
|
|
808
913
|
---
|
|
809
914
|
|
|
@@ -858,7 +963,10 @@ The same developer source compiles for two runtimes via `src/server/adapter/`:
|
|
|
858
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.
|
|
859
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.
|
|
860
965
|
|
|
861
|
-
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.
|
|
862
970
|
|
|
863
971
|
---
|
|
864
972
|
|
|
@@ -887,7 +995,8 @@ Developer-facing APIs (`createTypedDB`, `subscribeDoc`, `createStorageClient`, `
|
|
|
887
995
|
| `NEON_PROXY_URL` | Set by `dev:workers` — Neon-wire endpoint the local Neon HTTP driver dials. |
|
|
888
996
|
| `CLOUDFLARE_WORKERS` | Set to `1` inside the Workers adapter for runtime detection. |
|
|
889
997
|
|
|
890
|
-
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_*`.
|
|
891
1000
|
|
|
892
1001
|
---
|
|
893
1002
|
|
|
@@ -912,7 +1021,8 @@ Browser-visible variables must be prefixed `VITE_` and consumed via `import.meta
|
|
|
912
1021
|
| `ugly-app feedback:dev` / `feedback:prod` | Query user feedback. |
|
|
913
1022
|
| `ugly-app feedback:submit` / `feedback:resolve` | Manage feedback (run with `--help` for flags). |
|
|
914
1023
|
|
|
915
|
-
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`.
|
|
916
1026
|
|
|
917
1027
|
---
|
|
918
1028
|
|
|
@@ -925,10 +1035,13 @@ Schema changes must be deliberate:
|
|
|
925
1035
|
3. Replace every `REPLACE_ME` with the correct migration logic.
|
|
926
1036
|
4. Run `npm run db:migrate`.
|
|
927
1037
|
|
|
928
|
-
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).
|
|
929
1040
|
|
|
930
1041
|
---
|
|
931
1042
|
|
|
932
1043
|
## Tech stack
|
|
933
1044
|
|
|
934
|
-
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"}
|