ugly-app 0.1.42 → 0.1.44
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 +160 -28
- package/dist/cli/scaffold.d.ts.map +1 -1
- package/dist/cli/scaffold.js +34 -8
- package/dist/cli/scaffold.js.map +1 -1
- package/dist/client/callAI.d.ts.map +1 -1
- package/dist/client/callAI.js +7 -1
- package/dist/client/callAI.js.map +1 -1
- package/package.json +1 -1
- package/templates/client/pages/AITestPage.tsx +3 -3
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@ A full-stack TypeScript framework for building production-ready web applications
|
|
|
6
6
|
|
|
7
7
|
- **Server**: Express + WebSocket with type-safe RPC and Zod validation
|
|
8
8
|
- **Client**: React + Vite with typed routing, lazy pages, and popup management
|
|
9
|
-
- **Database**: MongoDB with typed collections, indexes, migrations, and live document tracking
|
|
9
|
+
- **Database**: MongoDB with typed collections, dot-notation updates, indexes, migrations, and live document tracking
|
|
10
10
|
- **Auth**: JWT + HttpOnly cookies, ugly.bot OAuth out of the box, extensible via `AuthProvider`
|
|
11
|
-
- **AI**: Text generation (Together, Claude, OpenAI, Google, Groq, Fireworks) + image generation (Together, FAL, Google, Wavespeed)
|
|
11
|
+
- **AI**: Text generation (Together, Claude, OpenAI, Google, Groq, Fireworks) + image generation (Together, FAL, Google, Wavespeed) + embeddings + STT/TTS
|
|
12
12
|
- **Storage**: Cloudflare R2 / AWS S3 with presigned uploads
|
|
13
13
|
- **CLI**: `ugly-app` commands for dev, build, deploy, migrations, logs, and auth utilities
|
|
14
14
|
|
|
@@ -143,7 +143,7 @@ export const requests = defineRequests({
|
|
|
143
143
|
|
|
144
144
|
- `fn({ input, output })` — defines a mutation (write operation). `input` and `output` are Zod schemas.
|
|
145
145
|
- `req({ input, output })` — defines a query (read operation).
|
|
146
|
-
- `defineFunctions()` / `defineRequests()` — identity wrappers that preserve types.
|
|
146
|
+
- `defineFunctions()` / `defineRequests()` — identity wrappers that preserve types. `defineCalls` is an alias for `defineFunctions`.
|
|
147
147
|
- `z` is re-exported from Zod for convenience.
|
|
148
148
|
|
|
149
149
|
### Collections (`shared/collections.ts`)
|
|
@@ -164,11 +164,14 @@ export const collections = defineCollections({
|
|
|
164
164
|
});
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
- `
|
|
169
|
-
- `
|
|
170
|
-
- `
|
|
171
|
-
- `
|
|
167
|
+
Each collection definition has:
|
|
168
|
+
- `type` — phantom field for TypeScript type inference (never read at runtime)
|
|
169
|
+
- `meta` — runtime metadata:
|
|
170
|
+
- `cache` — enable in-memory caching
|
|
171
|
+
- `trackable` — allow real-time `trackDoc`/`trackDocs` subscriptions
|
|
172
|
+
- `public` — accessible without auth
|
|
173
|
+
- `parent` — parent collection name for cascade deletes (or `null`)
|
|
174
|
+
- `onDelete?` — optional callback invoked on document deletion
|
|
172
175
|
|
|
173
176
|
All documents extend `DBObject`: `{ id: string, version: number, created: Date, updated: Date }`.
|
|
174
177
|
|
|
@@ -282,6 +285,10 @@ Wraps your app with context for `useApp()`. Provides user info, socket access, p
|
|
|
282
285
|
| `splashDone(step)` | Mark a splash screen step as complete |
|
|
283
286
|
| `localizer(key, params?)` | Localize a string key |
|
|
284
287
|
|
|
288
|
+
`useAppOptional()` returns the same context or `null` if outside `<AppProvider>`.
|
|
289
|
+
|
|
290
|
+
`useLocalizer()` returns the localizer function (falls back to identity if outside provider).
|
|
291
|
+
|
|
285
292
|
### Router
|
|
286
293
|
|
|
287
294
|
#### Setup (`client/router.ts`)
|
|
@@ -315,7 +322,18 @@ export const allPages = {
|
|
|
315
322
|
```
|
|
316
323
|
|
|
317
324
|
- **`lazyPage(factory)`** — lazy-imports a default-exported React component. The component receives route params as props.
|
|
318
|
-
- **`lazyPageLoader(factory)`** — lazy-imports an async loader function `(params) => Promise<ReactElement>` for routes that need data fetching before render. The loader file is the chunk boundary.
|
|
325
|
+
- **`lazyPageLoader(factory)`** — lazy-imports an async loader function `(params) => Promise<ReactElement>` for routes that need data fetching before render. The loader file is the chunk boundary — it can statically import its page component.
|
|
326
|
+
|
|
327
|
+
**`lazyPageLoader` example:**
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
// pages/SlowPageLoader.tsx
|
|
331
|
+
import SlowPage from './SlowPage'; // static import OK — same chunk
|
|
332
|
+
export default async function PageLoader({ id }: { id: string }) {
|
|
333
|
+
const data = await fetchSlowData(id);
|
|
334
|
+
return <SlowPage {...data} />;
|
|
335
|
+
}
|
|
336
|
+
```
|
|
319
337
|
|
|
320
338
|
#### Navigation with `useRouter()`
|
|
321
339
|
|
|
@@ -354,10 +372,12 @@ handle.hide(); // dismiss programmatically
|
|
|
354
372
|
```
|
|
355
373
|
|
|
356
374
|
Popup modes:
|
|
357
|
-
- **`block`** — dark backdrop (40% opacity), clicking backdrop does NOT dismiss
|
|
375
|
+
- **`block`** (default) — dark backdrop (40% opacity), clicking backdrop does NOT dismiss
|
|
358
376
|
- **`transient`** — light backdrop (20% opacity), clicking backdrop dismisses
|
|
359
377
|
- **`contextMenu`** — same as transient, intended for menus and pickers
|
|
360
378
|
|
|
379
|
+
`renderLayer` receives `{ content, spring, hide }` — `spring` is an animated value from 0 to 1, `hide` is a function to close the popup.
|
|
380
|
+
|
|
361
381
|
#### `Link` component
|
|
362
382
|
|
|
363
383
|
```tsx
|
|
@@ -370,6 +390,49 @@ import { Link } from 'ugly-app/client';
|
|
|
370
390
|
|
|
371
391
|
Renders an `<a>` tag with the correct `href`. Intercepts clicks for client-side navigation (ctrl/cmd+click opens in new tab).
|
|
372
392
|
|
|
393
|
+
#### Animation system
|
|
394
|
+
|
|
395
|
+
Built-in spring-like animation primitives for transitions and popups.
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
import {
|
|
399
|
+
Animated,
|
|
400
|
+
createAnimatedValue,
|
|
401
|
+
useAnimatedValue,
|
|
402
|
+
easingFunctions,
|
|
403
|
+
FadeIn,
|
|
404
|
+
SlideFromBottom,
|
|
405
|
+
SlideFromRight,
|
|
406
|
+
} from 'ugly-app/client';
|
|
407
|
+
|
|
408
|
+
// Create an animated value (0→1 spring)
|
|
409
|
+
const spring = createAnimatedValue(0);
|
|
410
|
+
spring.start(1, { duration: 300, easing: easingFunctions.easeOut });
|
|
411
|
+
|
|
412
|
+
// Use in components
|
|
413
|
+
<Animated.div style={{ opacity: spring.to((v) => String(v)) }}>
|
|
414
|
+
Content
|
|
415
|
+
</Animated.div>
|
|
416
|
+
|
|
417
|
+
// Hook version — creates and manages an animated value in a component
|
|
418
|
+
const anim = useAnimatedValue(0);
|
|
419
|
+
|
|
420
|
+
// Pre-built entrance animations (wrap any children)
|
|
421
|
+
<FadeIn>{children}</FadeIn>
|
|
422
|
+
<SlideFromBottom>{children}</SlideFromBottom>
|
|
423
|
+
<SlideFromRight>{children}</SlideFromRight>
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
Available easings: `easingFunctions.linear`, `easeIn`, `easeOut`, `easeInOut`.
|
|
427
|
+
|
|
428
|
+
#### Screenshot capture
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
import { captureScreenshot } from 'ugly-app/client';
|
|
432
|
+
|
|
433
|
+
const dataUrl = await captureScreenshot(); // captures the current viewport
|
|
434
|
+
```
|
|
435
|
+
|
|
373
436
|
---
|
|
374
437
|
|
|
375
438
|
## Auth
|
|
@@ -423,7 +486,7 @@ configurator.setAuth({
|
|
|
423
486
|
await ctx.db.setDoc(collections.note, doc);
|
|
424
487
|
await ctx.db.setDoc(collections.note, doc, { skipIfExists: true });
|
|
425
488
|
|
|
426
|
-
// Partial update — only specified fields
|
|
489
|
+
// Partial update — only specified fields (supports dot-notation paths)
|
|
427
490
|
await ctx.db.setDocFields(collections.note, id, { title: 'New title' });
|
|
428
491
|
|
|
429
492
|
// Partial update — returns null if document doesn't exist (no error)
|
|
@@ -447,6 +510,10 @@ const notes = await ctx.db.getDocs(collections.note, { userId }, { sort: { creat
|
|
|
447
510
|
const results = await ctx.db.getQuery<MyResult>('note', pipeline, { skip, limit });
|
|
448
511
|
const count = await ctx.db.getQueryCount('note', pipeline);
|
|
449
512
|
const raw = await ctx.db.getQueryRaw<T>('note', pipeline);
|
|
513
|
+
|
|
514
|
+
// Dynamic/untyped access (when collection name is a runtime string)
|
|
515
|
+
const doc = await ctx.db.rawGetDoc(collectionName, id);
|
|
516
|
+
const docs = await ctx.db.rawGetDocs(collectionName, filter);
|
|
450
517
|
```
|
|
451
518
|
|
|
452
519
|
### Deleting
|
|
@@ -474,7 +541,7 @@ import { dbDefaults } from 'ugly-app/shared';
|
|
|
474
541
|
// dbDefaults() returns { version: 1, created: new Date(), updated: new Date() }
|
|
475
542
|
const doc = { id: newId(), ...dbDefaults(), title: 'Hello' };
|
|
476
543
|
|
|
477
|
-
// createUserHelper — typed user CRUD
|
|
544
|
+
// createUserHelper — typed user CRUD with get, set, update methods
|
|
478
545
|
const userHelper = createUserHelper<User>(collections.user);
|
|
479
546
|
const user = await userHelper.get(db, userId);
|
|
480
547
|
await userHelper.set(db, { id: userId, ...dbDefaults(), email });
|
|
@@ -494,6 +561,8 @@ export const dbIndexes = defineDbIndexes({
|
|
|
494
561
|
});
|
|
495
562
|
```
|
|
496
563
|
|
|
564
|
+
Index types supported: standard (`IndexDef`), text search (`SearchIndexDef`), and vector/embedding (`VectorIndexDef` with cosine, euclidean, or dotProduct similarity).
|
|
565
|
+
|
|
497
566
|
Run `npm run db:init` to create/update indexes.
|
|
498
567
|
|
|
499
568
|
---
|
|
@@ -547,13 +616,33 @@ import { createImageGenClient } from 'ugly-app';
|
|
|
547
616
|
const imageGen = createImageGenClient();
|
|
548
617
|
```
|
|
549
618
|
|
|
619
|
+
### Embeddings
|
|
620
|
+
|
|
621
|
+
```typescript
|
|
622
|
+
import { createEmbeddingClient, cosineSimilarity } from 'ugly-app';
|
|
623
|
+
const embeddings = createEmbeddingClient();
|
|
624
|
+
const similarity = cosineSimilarity(vectorA, vectorB);
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Speech-to-text / Text-to-speech
|
|
628
|
+
|
|
629
|
+
```typescript
|
|
630
|
+
// Server-side providers
|
|
631
|
+
import { registerSTTProvider, groqWhisperSTTProvider, loadVADModel } from 'ugly-app';
|
|
632
|
+
import { registerTTSProvider, azureTTSProvider } from 'ugly-app';
|
|
633
|
+
|
|
634
|
+
// Client-side hooks
|
|
635
|
+
import { useSTT, useTTS, AudioPlayer, AudioRecorder } from 'ugly-app/client';
|
|
636
|
+
```
|
|
637
|
+
|
|
550
638
|
### Custom providers
|
|
551
639
|
|
|
552
640
|
```typescript
|
|
553
|
-
import { registerTextGenProvider, registerImageGenProvider } from 'ugly-app';
|
|
641
|
+
import { registerTextGenProvider, registerImageGenProvider, registerEmbeddingProvider } from 'ugly-app';
|
|
554
642
|
|
|
555
643
|
registerTextGenProvider('myProvider', myTextGenImplementation);
|
|
556
644
|
registerImageGenProvider('myProvider', myImageGenImplementation);
|
|
645
|
+
registerEmbeddingProvider('myProvider', myEmbeddingImplementation);
|
|
557
646
|
```
|
|
558
647
|
|
|
559
648
|
### Client-side AI calls
|
|
@@ -588,7 +677,7 @@ const url = storage.url('public', destKey);
|
|
|
588
677
|
const { uploadUrl, resultUrl } = await storage.presignedPut('temp', key);
|
|
589
678
|
```
|
|
590
679
|
|
|
591
|
-
Buckets: `'public'` and `'temp'`.
|
|
680
|
+
Buckets: `'public'` and `'temp'`. Supports Cloudflare R2 (production) or MinIO (dev).
|
|
592
681
|
|
|
593
682
|
Static build-time assets go in `client/public/`. Never hardcode `/asset/...` paths — use the `buildId` from `shared/Build.ts`.
|
|
594
683
|
|
|
@@ -626,7 +715,7 @@ sub(); // unsubscribe
|
|
|
626
715
|
### Redis
|
|
627
716
|
|
|
628
717
|
```typescript
|
|
629
|
-
import { getRedisClient } from 'ugly-app';
|
|
718
|
+
import { getRedisClient, redisGet, redisSet, redisDel, redisPublish, redisSubscribe } from 'ugly-app';
|
|
630
719
|
const redis = getRedisClient();
|
|
631
720
|
```
|
|
632
721
|
|
|
@@ -641,6 +730,15 @@ configurator.setWorkerQueue(queue);
|
|
|
641
730
|
await enqueueTask('taskName', payload);
|
|
642
731
|
```
|
|
643
732
|
|
|
733
|
+
### Billing
|
|
734
|
+
|
|
735
|
+
```typescript
|
|
736
|
+
import { initBillingGateway, getBillingGateway } from 'ugly-app';
|
|
737
|
+
|
|
738
|
+
await initBillingGateway({ /* config */ });
|
|
739
|
+
const billing = getBillingGateway();
|
|
740
|
+
```
|
|
741
|
+
|
|
644
742
|
### Client error capture
|
|
645
743
|
|
|
646
744
|
```typescript
|
|
@@ -672,21 +770,20 @@ Server-side, `getFeedbackHandlers(maintainBotUserId)` provides the RPC handlers
|
|
|
672
770
|
await ctx.rateLimit.check();
|
|
673
771
|
```
|
|
674
772
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
```typescript
|
|
678
|
-
import { createEmbeddingClient, registerEmbeddingProvider } from 'ugly-app';
|
|
679
|
-
const embeddings = createEmbeddingClient();
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
### Billing
|
|
773
|
+
---
|
|
683
774
|
|
|
684
|
-
|
|
685
|
-
import { initBillingGateway, getBillingGateway } from 'ugly-app';
|
|
775
|
+
## Built-in endpoints
|
|
686
776
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
777
|
+
| Endpoint | Description |
|
|
778
|
+
|----------|-------------|
|
|
779
|
+
| `GET /health` | Health check — returns `{ status: 'ok', timestamp }` |
|
|
780
|
+
| `POST /auth/verify` | Exchange OAuth code for a session cookie |
|
|
781
|
+
| `POST /auth/logout` | Clear the auth cookie |
|
|
782
|
+
| `GET /auth/token` | Refresh and return the current token |
|
|
783
|
+
| `GET /auth/url` | Get the OAuth popup URL |
|
|
784
|
+
| `POST /ai/request` | AI proxy — forwards to ugly.bot (requires auth) |
|
|
785
|
+
| `POST /api/client-error` | Client-side error capture |
|
|
786
|
+
| `GET /my_feedback` | User feedback history (markdown, requires auth) |
|
|
690
787
|
|
|
691
788
|
---
|
|
692
789
|
|
|
@@ -734,6 +831,7 @@ Run with `npm run db:migrate`. Use `npm run db:migrate -- --status` to preview p
|
|
|
734
831
|
| Variable | Description |
|
|
735
832
|
|----------|-------------|
|
|
736
833
|
| `JWT_SECRET` | **Required** — signs auth tokens |
|
|
834
|
+
| `JWT_EXPIRY_SECONDS` | Token lifetime (optional) |
|
|
737
835
|
| `MONGODB_URI` | MongoDB connection string |
|
|
738
836
|
| `PORT` | Server port (default: 3000) |
|
|
739
837
|
| `NODE_ENV` | `development` or `production` |
|
|
@@ -761,3 +859,37 @@ Client-side variables must be prefixed with `VITE_`.
|
|
|
761
859
|
## Tech stack
|
|
762
860
|
|
|
763
861
|
Node.js · TypeScript · Express · React 19 · Vite · Tailwind CSS · MongoDB · NATS · Redis · Cloudflare R2 · Zod · JWT (jose) · ugly.bot OAuth
|
|
862
|
+
|
|
863
|
+
---
|
|
864
|
+
|
|
865
|
+
## Shared utilities
|
|
866
|
+
|
|
867
|
+
`ugly-app/shared` exports common helpers used by both server and client:
|
|
868
|
+
|
|
869
|
+
```typescript
|
|
870
|
+
import {
|
|
871
|
+
isDefined,
|
|
872
|
+
compact,
|
|
873
|
+
debounce,
|
|
874
|
+
formatDate,
|
|
875
|
+
formatRelativeTime,
|
|
876
|
+
oneSecond,
|
|
877
|
+
oneMinute,
|
|
878
|
+
oneHour,
|
|
879
|
+
oneDay,
|
|
880
|
+
oneWeek,
|
|
881
|
+
} from 'ugly-app/shared';
|
|
882
|
+
|
|
883
|
+
isDefined(value); // type guard — true if not null/undefined
|
|
884
|
+
compact([1, null, 2, undefined]); // [1, 2] — filters out null/undefined
|
|
885
|
+
const debouncedFn = debounce(fn, 300);
|
|
886
|
+
formatDate(new Date()); // locale-formatted date string
|
|
887
|
+
formatRelativeTime(new Date()); // "2 hours ago", "just now", etc.
|
|
888
|
+
|
|
889
|
+
// Time constants (milliseconds)
|
|
890
|
+
oneSecond; // 1000
|
|
891
|
+
oneMinute; // 60_000
|
|
892
|
+
oneHour; // 3_600_000
|
|
893
|
+
oneDay; // 86_400_000
|
|
894
|
+
oneWeek; // 604_800_000
|
|
895
|
+
```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/cli/scaffold.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../src/cli/scaffold.ts"],"names":[],"mappings":"AAyFA,wBAAsB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4DxE"}
|
package/dist/cli/scaffold.js
CHANGED
|
@@ -38,7 +38,33 @@ function generateEnv(examplePath, projectName) {
|
|
|
38
38
|
})
|
|
39
39
|
.join('\n');
|
|
40
40
|
}
|
|
41
|
+
/** Locate the `code` binary — checks PATH then known macOS app bundle locations. */
|
|
42
|
+
function findCodeCli() {
|
|
43
|
+
try {
|
|
44
|
+
execSync('code --version', { stdio: 'ignore' });
|
|
45
|
+
return 'code';
|
|
46
|
+
}
|
|
47
|
+
catch { /* not in PATH */ }
|
|
48
|
+
const candidates = process.platform === 'win32'
|
|
49
|
+
? [
|
|
50
|
+
path.join(process.env['LOCALAPPDATA'] ?? '', 'Programs\\Microsoft VS Code\\bin\\code.cmd'),
|
|
51
|
+
path.join(process.env['ProgramFiles'] ?? '', 'Microsoft VS Code\\bin\\code.cmd'),
|
|
52
|
+
path.join(process.env['ProgramFiles(x86)'] ?? '', 'Microsoft VS Code\\bin\\code.cmd'),
|
|
53
|
+
]
|
|
54
|
+
: [
|
|
55
|
+
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code',
|
|
56
|
+
path.join(os.homedir(), 'Applications/Visual Studio Code.app/Contents/Resources/app/bin/code'),
|
|
57
|
+
];
|
|
58
|
+
for (const p of candidates) {
|
|
59
|
+
if (fs.existsSync(p))
|
|
60
|
+
return `"${p}"`;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
41
64
|
async function installVsixExtension() {
|
|
65
|
+
const codeCli = findCodeCli();
|
|
66
|
+
if (!codeCli)
|
|
67
|
+
return; // VSCode not found — skip silently
|
|
42
68
|
const url = 'https://ugly.bot/asset/ugly-bot.vsix';
|
|
43
69
|
const vsixPath = path.join(os.tmpdir(), 'ugly-bot.vsix');
|
|
44
70
|
try {
|
|
@@ -48,11 +74,11 @@ async function installVsixExtension() {
|
|
|
48
74
|
throw new Error(`HTTP ${res.status}`);
|
|
49
75
|
const buf = await res.arrayBuffer();
|
|
50
76
|
await fs.writeFile(vsixPath, Buffer.from(buf));
|
|
51
|
-
execSync(
|
|
77
|
+
execSync(`${codeCli} --install-extension "${vsixPath}"`, { stdio: 'inherit' });
|
|
52
78
|
console.log('[ugly-app] VSCode extension installed.');
|
|
53
79
|
}
|
|
54
|
-
catch {
|
|
55
|
-
console.warn(
|
|
80
|
+
catch (err) {
|
|
81
|
+
console.warn(`[ugly-app] Could not install VSCode extension (skipping): ${err.message}`);
|
|
56
82
|
}
|
|
57
83
|
finally {
|
|
58
84
|
await fs.remove(vsixPath).catch(() => { });
|
|
@@ -77,6 +103,11 @@ export async function scaffoldProject(projectName) {
|
|
|
77
103
|
await fs.writeFile(path.join(destDir, '.env'), envContent, 'utf-8');
|
|
78
104
|
console.log('[ugly-app] Installing dependencies...');
|
|
79
105
|
execSync('npm install', { cwd: destDir, stdio: 'inherit' });
|
|
106
|
+
const portStart = 3000;
|
|
107
|
+
const projectId = nanoid(10).toLowerCase();
|
|
108
|
+
writeUglyAppConfig(destDir, { projectId, portStart });
|
|
109
|
+
console.log(`\n[uglyapp] Generated .uglyapp for ${projectName}`);
|
|
110
|
+
printPortTable(portStart);
|
|
80
111
|
console.log('[ugly-app] Initialising git repository...');
|
|
81
112
|
execSync('git init', { cwd: destDir, stdio: 'inherit' });
|
|
82
113
|
execSync('git add .', { cwd: destDir, stdio: 'inherit' });
|
|
@@ -84,11 +115,6 @@ export async function scaffoldProject(projectName) {
|
|
|
84
115
|
cwd: destDir,
|
|
85
116
|
stdio: 'inherit',
|
|
86
117
|
});
|
|
87
|
-
const portStart = 3000;
|
|
88
|
-
const projectId = nanoid(10).toLowerCase();
|
|
89
|
-
writeUglyAppConfig(destDir, { projectId, portStart });
|
|
90
|
-
console.log(`\n[uglyapp] Generated .uglyapp for ${projectName}`);
|
|
91
|
-
printPortTable(portStart);
|
|
92
118
|
console.log('Edit .uglyapp to change portStart if running multiple projects simultaneously.');
|
|
93
119
|
const envFilePath = path.join(destDir, '.env');
|
|
94
120
|
await runUglyBotSetup(projectId, envFilePath);
|
package/dist/cli/scaffold.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/cli/scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExE,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC/D,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC;AAE9D,gFAAgF;AAChF,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IAC5B,YAAY;IACZ,YAAY;IACZ,UAAU;IACV,MAAM;IACN,aAAa;IACb,kBAAkB;IAClB,uBAAuB;CACxB,CAAC,CAAC;AAEH,SAAS,WAAW,CAAC,WAAmB,EAAE,WAAmB;IAC3D,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAChE,OAAO,KAAK;SACT,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACZ,6BAA6B;QAC7B,IAAI,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;YACnC,OAAO,cAAc,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;QACpC,CAAC;QACD,yEAAyE;QACzE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;QACtD,IAAI,KAAK,EAAE,CAAC;YACV,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC;YAC7B,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBACnD,OAAO,KAAK,IAAI,EAAE,CAAC;YACrB,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;AAChB,CAAC;AAED,KAAK,UAAU,oBAAoB;IACjC,MAAM,GAAG,GAAG,sCAAsC,CAAC;IACnD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,eAAe,CAAC,CAAC;IACzD,IAAI,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,oDAAoD,CAAC,CAAC;QAClE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QACnD,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC;QACpC,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC/C,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../src/cli/scaffold.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"callAI.d.ts","sourceRoot":"","sources":["../../src/client/callAI.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACb,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"callAI.d.ts","sourceRoot":"","sources":["../../src/client/callAI.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,YAAY,EACZ,YAAY,EACb,MAAM,oBAAoB,CAAC;AA0B5B,eAAO,MAAM,WAAW,GAAI,OAAO,YAAY,KAAG,OAAO,CAAC,MAAM,CACZ,CAAC;AAErD,eAAO,MAAM,WAAW,GAAI,CAAC,GAAG,OAAO,EAAE,OAAO,YAAY,KAAG,OAAO,CAAC,CAAC,CACzB,CAAC;AAEhD,eAAO,MAAM,YAAY,GAAI,OAAO,aAAa,KAAG,OAAO,CAAC,MAAM,CACb,CAAC"}
|
package/dist/client/callAI.js
CHANGED
|
@@ -15,7 +15,13 @@ async function callViaServer(op, input) {
|
|
|
15
15
|
const body = await res.text().catch(() => '');
|
|
16
16
|
throw new Error(`${res.status}: ${body}`);
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
const json = (await res.json());
|
|
19
|
+
console.log('[callAI]', op, json);
|
|
20
|
+
if (json.error)
|
|
21
|
+
throw new Error(json.error);
|
|
22
|
+
if (json.result === undefined)
|
|
23
|
+
throw new Error('AI request returned no result');
|
|
24
|
+
return json.result;
|
|
19
25
|
}
|
|
20
26
|
export const callTextGen = (input) => callViaServer('textGen', input);
|
|
21
27
|
export const callJsonGen = (input) => callViaServer('jsonGen', input);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"callAI.js","sourceRoot":"","sources":["../../src/client/callAI.ts"],"names":[],"mappings":"AAMA,2EAA2E;AAC3E,+CAA+C;AAC/C,KAAK,UAAU,aAAa,CAAC,EAAU,EAAE,KAAc;IACrD,MAAM,KAAK,GAAI,MAAiD;SAC7D,cAAc,CAAC;IAClB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,aAAa,EAAE;QACrC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,KAAK,IAAI,EAAE,EAAE;YACtC,cAAc,EAAE,kBAAkB;SACnC;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;KACzD,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC;IAC5C,CAAC;IACD,
|
|
1
|
+
{"version":3,"file":"callAI.js","sourceRoot":"","sources":["../../src/client/callAI.ts"],"names":[],"mappings":"AAMA,2EAA2E;AAC3E,+CAA+C;AAC/C,KAAK,UAAU,aAAa,CAAC,EAAU,EAAE,KAAc;IACrD,MAAM,KAAK,GAAI,MAAiD;SAC7D,cAAc,CAAC;IAClB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,aAAa,EAAE;QACrC,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,aAAa,EAAE,UAAU,KAAK,IAAI,EAAE,EAAE;YACtC,cAAc,EAAE,kBAAkB;SACnC;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;KACzD,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAC9C,MAAM,IAAI,KAAK,CAAC,GAAG,GAAG,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAyC,CAAC;IACxE,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,EAAE,EAAE,IAAI,CAAC,CAAC;IAClC,IAAI,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5C,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IAChF,OAAO,IAAI,CAAC,MAAM,CAAC;AACrB,CAAC;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAmB,EAAmB,EAAE,CAClE,aAAa,CAAC,SAAS,EAAE,KAAK,CAAoB,CAAC;AAErD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAc,KAAmB,EAAc,EAAE,CAC1E,aAAa,CAAC,SAAS,EAAE,KAAK,CAAe,CAAC;AAEhD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAoB,EAAmB,EAAE,CACpE,aAAa,CAAC,UAAU,EAAE,KAAK,CAAoB,CAAC"}
|
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ import type { ImageGenModel, TextGenModel } from 'ugly-app/shared';
|
|
|
11
11
|
import { imageGenModels, textGenModels } from 'ugly-app/shared';
|
|
12
12
|
|
|
13
13
|
type Mode = 'text' | 'image';
|
|
14
|
-
|
|
14
|
+
interface LogEntry { ts: number; msg: string; kind: 'info' | 'ok' | 'err' }
|
|
15
15
|
|
|
16
16
|
function fmt(ms: number): string {
|
|
17
17
|
return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
@@ -52,8 +52,8 @@ export default function AITestPage(): React.ReactElement {
|
|
|
52
52
|
messages: [{ role: 'user', content: prompt }],
|
|
53
53
|
});
|
|
54
54
|
const elapsed = Date.now() - started;
|
|
55
|
-
addLog(`Done in ${fmt(elapsed)} — ${text
|
|
56
|
-
setResult(text);
|
|
55
|
+
addLog(`Done in ${fmt(elapsed)} — ${text?.length ?? 0} chars`, 'ok');
|
|
56
|
+
setResult(text ?? '');
|
|
57
57
|
} else {
|
|
58
58
|
const url = await callImageGen({ model: imageModel, prompt });
|
|
59
59
|
const elapsed = Date.now() - started;
|