ugly-app 0.1.240 → 0.1.241

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.
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAIA,wBAAgB,QAAQ,IAAI,IAAI,CA+C/B"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAIA,wBAAgB,QAAQ,IAAI,IAAI,CAsE/B"}
package/dist/cli/build.js CHANGED
@@ -33,6 +33,25 @@ export const buildId = '${buildId}';
33
33
  fs.mkdirSync(serverOutDir, { recursive: true });
34
34
  execSync(`npx esbuild server/index.ts --bundle --platform=node --format=esm --outfile=dist/server/index.mjs --external:ugly-app --external:ugly-app/*`, { stdio: 'inherit', cwd: process.cwd() });
35
35
  }
36
+ // Bundle task.ts if it exists (for cron/worker/GPU tasks)
37
+ const taskEntry = path.join(process.cwd(), 'server', 'task.ts');
38
+ if (fs.existsSync(taskEntry)) {
39
+ console.log('[Build] Bundling server/task.ts...');
40
+ const tasksOutDir = path.join(distDir, 'tasks');
41
+ fs.mkdirSync(tasksOutDir, { recursive: true });
42
+ // Externalize allowlisted packages — they'll be installed on the task host
43
+ const externals = [
44
+ 'ugly-app', 'ugly-app/*',
45
+ 'three', '@types/three', 'jsdom', 'gl',
46
+ '@ffmpeg-installer/ffmpeg', 'fluent-ffmpeg', 'sharp',
47
+ '@anthropic-ai/sdk', 'openai',
48
+ 'zod', 'date-fns', 'lodash',
49
+ 'mediasoup', 'mediasoup-client',
50
+ ];
51
+ const externalFlags = externals.map(e => `--external:${e}`).join(' ');
52
+ execSync(`npx esbuild server/task.ts --bundle --platform=node --format=esm --outfile=dist/tasks/task.js ${externalFlags}`, { stdio: 'inherit', cwd: process.cwd() });
53
+ console.log('[Build] Task bundle: dist/tasks/task.js');
54
+ }
36
55
  // Run tsc for typecheck
37
56
  console.log('[Build] Running tsc...');
38
57
  execSync('npx tsc --noEmit', { stdio: 'inherit', cwd: process.cwd() });
@@ -1 +1 @@
1
- {"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,UAAU,QAAQ;IACtB,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAEnC,wBAAwB;IACxB,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;IACrE,MAAM,gBAAgB,GAAG;0BACD,OAAO;CAChC,CAAC;IAEA,4BAA4B;IAC5B,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC9C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,CAAC,oBAAoB,OAAO,EAAE,CAAC,CAAC;IAE3C,8BAA8B;IAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;IACjD,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IACvC,CAAC;IAED,iBAAiB;IACjB,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;IAC7C,QAAQ,CAAC,gBAAgB,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAErE,oFAAoF;IACpF,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;IACnE,MAAM,oBAAoB,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,uBAAuB,CAAC,CAAC,CAAC;IAC9F,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC;QACxD,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;QAC1C,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAClD,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,QAAQ,CACN,6IAA6I,EAC7I,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CACzC,CAAC;IACJ,CAAC;IAED,wBAAwB;IACxB,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;IACtC,QAAQ,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAEvE,OAAO,CAAC,GAAG,CAAC,2BAA2B,OAAO,GAAG,CAAC,CAAC;AACrD,CAAC"}
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.1.240";
1
+ export declare const CLI_VERSION = "0.1.241";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.240";
2
+ export const CLI_VERSION = "0.1.241";
3
3
  //# sourceMappingURL=version.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * TaskContext — the execution environment for project task bundles.
3
+ *
4
+ * Every project's `task.js` exports an `execute(ctx, data)` function.
5
+ * The platform creates a TaskContext scoped to the project's own DB and R2,
6
+ * then calls execute(). The project never needs DB credentials or R2 keys
7
+ * — the platform provides them through this interface.
8
+ *
9
+ * ```typescript
10
+ * // server/task.ts (project source)
11
+ * import type { TaskContext } from 'ugly-app/worker';
12
+ *
13
+ * export async function execute(ctx: TaskContext, data: { taskName: string }) {
14
+ * switch (data.taskName) {
15
+ * case 'dailyCleanup':
16
+ * const old = await ctx.db.find('todo', { done: true, updated: { $lt: daysAgo(30) } });
17
+ * for (const doc of old) await ctx.db.deleteDoc('todo', doc._id);
18
+ * break;
19
+ * }
20
+ * }
21
+ * ```
22
+ */
23
+ export interface TaskContext {
24
+ db: TaskDB;
25
+ r2: TaskStorage;
26
+ tempR2: TaskTempStorage;
27
+ taskId: string;
28
+ taskName: string;
29
+ projectId: string;
30
+ working(): void;
31
+ log(message: string, data?: Record<string, unknown>): void;
32
+ }
33
+ export interface TaskDB {
34
+ find(collection: string, query: Record<string, unknown>): Promise<Record<string, unknown>[]>;
35
+ getDoc(collection: string, id: string): Promise<Record<string, unknown> | null>;
36
+ setDoc(collection: string, doc: Record<string, unknown>): Promise<void>;
37
+ setDocFields(collection: string, id: string, fields: Record<string, unknown>): Promise<void>;
38
+ deleteDoc(collection: string, id: string): Promise<void>;
39
+ deleteMany(collection: string, ids: string[]): Promise<void>;
40
+ count(collection: string, query: Record<string, unknown>): Promise<number>;
41
+ }
42
+ export interface TaskStorage {
43
+ get(key: string): Promise<Buffer | null>;
44
+ getJSON<T = unknown>(key: string): Promise<T | null>;
45
+ put(key: string, data: Buffer | string, opts?: {
46
+ contentType?: string;
47
+ }): Promise<string>;
48
+ delete(key: string): Promise<void>;
49
+ list(prefix: string): Promise<string[]>;
50
+ }
51
+ export interface TaskTempStorage {
52
+ get(key: string): Promise<Buffer | null>;
53
+ put(key: string, data: Buffer): Promise<string>;
54
+ }
55
+ /**
56
+ * The function signature that every project's task.js must export.
57
+ */
58
+ export type TaskExecuteFn = (ctx: TaskContext, data: Record<string, unknown>) => Promise<unknown>;
59
+ //# sourceMappingURL=TaskContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TaskContext.d.ts","sourceRoot":"","sources":["../../src/worker/TaskContext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,WAAW;IAE1B,EAAE,EAAE,MAAM,CAAC;IAGX,EAAE,EAAE,WAAW,CAAC;IAGhB,MAAM,EAAE,eAAe,CAAC;IAGxB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAGlB,OAAO,IAAI,IAAI,CAAC;IAGhB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC5D;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAC7F,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAChF,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7F,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5E;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACrD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1F,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACjD;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC1B,OAAO,CAAC,OAAO,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=TaskContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TaskContext.js","sourceRoot":"","sources":["../../src/worker/TaskContext.ts"],"names":[],"mappings":""}
@@ -0,0 +1,74 @@
1
+ import type { TaskDB, TaskStorage, TaskTempStorage } from './TaskContext.js';
2
+ /**
3
+ * NPM packages that task bundles are allowed to install.
4
+ * Any package not on this list is rejected.
5
+ */
6
+ export declare const ALLOWED_PACKAGES: readonly ["three", "@types/three", "jsdom", "gl", "@ffmpeg-installer/ffmpeg", "fluent-ffmpeg", "sharp", "@anthropic-ai/sdk", "openai", "zod", "date-fns", "lodash", "mediasoup", "mediasoup-client", "ugly-app"];
7
+ /**
8
+ * Configuration for the task host.
9
+ */
10
+ export interface TaskHostConfig {
11
+ /**
12
+ * Factory to create a scoped DB proxy for a project.
13
+ * The proxy restricts access to the project's own PostgreSQL user/database.
14
+ */
15
+ createDbProxy(projectId: string): TaskDB;
16
+ /**
17
+ * Factory to create a scoped R2 proxy for a project.
18
+ * Restricts access to the project's own R2 bucket.
19
+ */
20
+ createR2Proxy(projectId: string): TaskStorage;
21
+ /**
22
+ * Factory to create a temp R2 proxy for a project.
23
+ */
24
+ createTempR2Proxy(projectId: string): TaskTempStorage;
25
+ /**
26
+ * Fetch a task bundle from R2 by URL.
27
+ * Returns the JavaScript source code as a string.
28
+ */
29
+ fetchBundle(codeUrl: string): Promise<string>;
30
+ /** Working directory for task bundles. Default: os.tmpdir()/ugly-tasks/ */
31
+ workDir?: string;
32
+ }
33
+ /**
34
+ * TaskHost — loads project task.js bundles from R2, caches warm processes,
35
+ * and executes tasks with scoped DB/R2 access.
36
+ */
37
+ export declare class TaskHost {
38
+ private config;
39
+ private cleanupTimer;
40
+ private baseWorkDir;
41
+ constructor(config: TaskHostConfig);
42
+ /**
43
+ * Execute a task using a cached or freshly loaded task bundle.
44
+ */
45
+ execute(opts: {
46
+ codeUrl: string;
47
+ projectId: string;
48
+ taskId: string;
49
+ taskName: string;
50
+ data?: Record<string, unknown>;
51
+ timeoutMs?: number;
52
+ }): Promise<unknown>;
53
+ /**
54
+ * Get a cached process for this code URL, or create one.
55
+ */
56
+ private getOrCreateProcess;
57
+ /**
58
+ * Install only allowlisted packages from a package.json.
59
+ */
60
+ private installAllowedPackages;
61
+ /**
62
+ * Evict idle processes (no invocation for IDLE_TIMEOUT_MS).
63
+ */
64
+ private cleanupIdle;
65
+ /**
66
+ * Invalidate a cached process (e.g., when buildId changes).
67
+ */
68
+ invalidate(codeUrl: string): void;
69
+ /**
70
+ * Shutdown — clean up all processes and timers.
71
+ */
72
+ shutdown(): Promise<void>;
73
+ }
74
+ //# sourceMappingURL=TaskHost.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TaskHost.d.ts","sourceRoot":"","sources":["../../src/worker/TaskHost.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAe,MAAM,EAAE,WAAW,EAAE,eAAe,EAAiB,MAAM,kBAAkB,CAAC;AAIzG;;;GAGG;AACH,eAAO,MAAM,gBAAgB,kNAanB,CAAC;AAgBX;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAEzC;;;OAGG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,CAAC;IAE9C;;OAEG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,CAAC;IAEtD;;;OAGG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE9C,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,YAAY,CAA+C;IACnE,OAAO,CAAC,WAAW,CAAS;gBAEhB,MAAM,EAAE,cAAc;IASlC;;OAEG;IACG,OAAO,CAAC,IAAI,EAAE;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,OAAO,CAAC;IAiCpB;;OAEG;YACW,kBAAkB;IA4ChC;;OAEG;YACW,sBAAsB;IA+BpC;;OAEG;IACH,OAAO,CAAC,WAAW;IAcnB;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAWjC;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAYhC"}
@@ -0,0 +1,187 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ /* eslint-disable @typescript-eslint/no-explicit-any */
5
+ /**
6
+ * NPM packages that task bundles are allowed to install.
7
+ * Any package not on this list is rejected.
8
+ */
9
+ export const ALLOWED_PACKAGES = [
10
+ // Rendering
11
+ 'three', '@types/three', 'jsdom', 'gl',
12
+ // Video/Audio
13
+ '@ffmpeg-installer/ffmpeg', 'fluent-ffmpeg', 'sharp',
14
+ // AI
15
+ '@anthropic-ai/sdk', 'openai',
16
+ // Data
17
+ 'zod', 'date-fns', 'lodash',
18
+ // WebRTC
19
+ 'mediasoup', 'mediasoup-client',
20
+ // Framework
21
+ 'ugly-app',
22
+ ];
23
+ const ALLOWED_SET = new Set(ALLOWED_PACKAGES);
24
+ const processCache = new Map();
25
+ const IDLE_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
26
+ /**
27
+ * TaskHost — loads project task.js bundles from R2, caches warm processes,
28
+ * and executes tasks with scoped DB/R2 access.
29
+ */
30
+ export class TaskHost {
31
+ config;
32
+ cleanupTimer = null;
33
+ baseWorkDir;
34
+ constructor(config) {
35
+ this.config = config;
36
+ this.baseWorkDir = config.workDir ?? path.join(os.tmpdir(), 'ugly-tasks');
37
+ fs.mkdirSync(this.baseWorkDir, { recursive: true });
38
+ // Periodic cleanup of idle processes
39
+ this.cleanupTimer = setInterval(() => this.cleanupIdle(), 60_000);
40
+ }
41
+ /**
42
+ * Execute a task using a cached or freshly loaded task bundle.
43
+ */
44
+ async execute(opts) {
45
+ const { codeUrl, projectId, taskId, taskName, data = {} } = opts;
46
+ // Get or create warm process
47
+ const proc = await this.getOrCreateProcess(codeUrl);
48
+ proc.lastUsed = Date.now();
49
+ // Create scoped TaskContext
50
+ const ctx = {
51
+ db: this.config.createDbProxy(projectId),
52
+ r2: this.config.createR2Proxy(projectId),
53
+ tempR2: this.config.createTempR2Proxy(projectId),
54
+ taskId,
55
+ taskName,
56
+ projectId,
57
+ working: () => { },
58
+ log: (message, logData) => {
59
+ console.log(`[Task:${projectId}/${taskName}] ${message}`, logData ?? '');
60
+ },
61
+ };
62
+ // Execute with timeout
63
+ const timeoutMs = opts.timeoutMs ?? 300_000; // 5 min default
64
+ const result = await Promise.race([
65
+ proc.executeFn(ctx, { taskName, ...data }),
66
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Task ${taskName} timed out after ${timeoutMs}ms`)), timeoutMs)),
67
+ ]);
68
+ return result;
69
+ }
70
+ /**
71
+ * Get a cached process for this code URL, or create one.
72
+ */
73
+ async getOrCreateProcess(codeUrl) {
74
+ const cached = processCache.get(codeUrl);
75
+ if (cached)
76
+ return cached;
77
+ // Fetch bundle
78
+ const source = await this.config.fetchBundle(codeUrl);
79
+ // Create working directory for this bundle
80
+ const hash = codeUrl.replace(/[^a-zA-Z0-9]/g, '_').slice(-64);
81
+ const workDir = path.join(this.baseWorkDir, hash);
82
+ fs.mkdirSync(workDir, { recursive: true });
83
+ // Write bundle to disk
84
+ const bundlePath = path.join(workDir, 'task.mjs');
85
+ fs.writeFileSync(bundlePath, source, 'utf-8');
86
+ // Install allowlisted packages if package.json exists alongside bundle
87
+ const pkgJsonUrl = codeUrl.replace(/task\.js$/, 'package.json');
88
+ try {
89
+ const pkgSource = await this.config.fetchBundle(pkgJsonUrl);
90
+ const pkg = JSON.parse(pkgSource);
91
+ await this.installAllowedPackages(workDir, pkg);
92
+ }
93
+ catch {
94
+ // No package.json — bundle is self-contained
95
+ }
96
+ // Dynamic import the module
97
+ const mod = await import(bundlePath);
98
+ if (typeof mod.execute !== 'function') {
99
+ throw new Error(`Task bundle at ${codeUrl} does not export an execute() function`);
100
+ }
101
+ const proc = {
102
+ codeUrl,
103
+ executeFn: mod.execute,
104
+ lastUsed: Date.now(),
105
+ workDir,
106
+ };
107
+ processCache.set(codeUrl, proc);
108
+ console.log(`[TaskHost] Loaded bundle: ${codeUrl}`);
109
+ return proc;
110
+ }
111
+ /**
112
+ * Install only allowlisted packages from a package.json.
113
+ */
114
+ async installAllowedPackages(workDir, pkg) {
115
+ if (!pkg.dependencies)
116
+ return;
117
+ const filtered = {};
118
+ for (const [name, version] of Object.entries(pkg.dependencies)) {
119
+ if (ALLOWED_SET.has(name)) {
120
+ filtered[name] = version;
121
+ }
122
+ else {
123
+ console.warn(`[TaskHost] Package "${name}" not in allowlist — skipped`);
124
+ }
125
+ }
126
+ if (Object.keys(filtered).length === 0)
127
+ return;
128
+ // Write filtered package.json
129
+ fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify({ dependencies: filtered }, null, 2));
130
+ // Install
131
+ const { execSync } = await import('child_process');
132
+ execSync('npm install --production --ignore-scripts', {
133
+ cwd: workDir,
134
+ stdio: 'pipe',
135
+ timeout: 120_000,
136
+ });
137
+ console.log(`[TaskHost] Installed ${Object.keys(filtered).length} allowlisted packages`);
138
+ }
139
+ /**
140
+ * Evict idle processes (no invocation for IDLE_TIMEOUT_MS).
141
+ */
142
+ cleanupIdle() {
143
+ const now = Date.now();
144
+ for (const [url, proc] of processCache) {
145
+ if (now - proc.lastUsed > IDLE_TIMEOUT_MS) {
146
+ processCache.delete(url);
147
+ // Clean up work directory
148
+ try {
149
+ fs.rmSync(proc.workDir, { recursive: true, force: true });
150
+ }
151
+ catch { /* ignore */ }
152
+ console.log(`[TaskHost] Evicted idle process: ${url}`);
153
+ }
154
+ }
155
+ }
156
+ /**
157
+ * Invalidate a cached process (e.g., when buildId changes).
158
+ */
159
+ invalidate(codeUrl) {
160
+ const proc = processCache.get(codeUrl);
161
+ if (proc) {
162
+ processCache.delete(codeUrl);
163
+ try {
164
+ fs.rmSync(proc.workDir, { recursive: true, force: true });
165
+ }
166
+ catch { /* ignore */ }
167
+ console.log(`[TaskHost] Invalidated: ${codeUrl}`);
168
+ }
169
+ }
170
+ /**
171
+ * Shutdown — clean up all processes and timers.
172
+ */
173
+ async shutdown() {
174
+ if (this.cleanupTimer) {
175
+ clearInterval(this.cleanupTimer);
176
+ this.cleanupTimer = null;
177
+ }
178
+ for (const [url, proc] of processCache) {
179
+ processCache.delete(url);
180
+ try {
181
+ fs.rmSync(proc.workDir, { recursive: true, force: true });
182
+ }
183
+ catch { /* ignore */ }
184
+ }
185
+ }
186
+ }
187
+ //# sourceMappingURL=TaskHost.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TaskHost.js","sourceRoot":"","sources":["../../src/worker/TaskHost.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export type { TaskContext, TaskDB, TaskStorage, TaskTempStorage, TaskExecuteFn, } from './TaskContext.js';
2
+ export { TaskHost, ALLOWED_PACKAGES, type TaskHostConfig, } from './TaskHost.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,WAAW,EACX,MAAM,EACN,WAAW,EACX,eAAe,EACf,aAAa,GACd,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { TaskHost, ALLOWED_PACKAGES, } from './TaskHost.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,QAAQ,EACR,gBAAgB,GAEjB,MAAM,eAAe,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugly-app",
3
- "version": "0.1.240",
3
+ "version": "0.1.241",
4
4
  "type": "module",
5
5
  "main": "./dist/server/index.js",
6
6
  "exports": {
@@ -16,6 +16,7 @@
16
16
  "default": "./dist/webrtc/index.js"
17
17
  },
18
18
  "./webrtc/server": "./dist/webrtc/server/index.js",
19
+ "./worker": "./dist/worker/index.js",
19
20
  "./vite": "./dist/vite/index.js",
20
21
  "./eslint": "./dist/eslint/index.js"
21
22
  },
package/src/cli/build.ts CHANGED
@@ -44,6 +44,29 @@ export const buildId = '${buildId}';
44
44
  );
45
45
  }
46
46
 
47
+ // Bundle task.ts if it exists (for cron/worker/GPU tasks)
48
+ const taskEntry = path.join(process.cwd(), 'server', 'task.ts');
49
+ if (fs.existsSync(taskEntry)) {
50
+ console.log('[Build] Bundling server/task.ts...');
51
+ const tasksOutDir = path.join(distDir, 'tasks');
52
+ fs.mkdirSync(tasksOutDir, { recursive: true });
53
+ // Externalize allowlisted packages — they'll be installed on the task host
54
+ const externals = [
55
+ 'ugly-app', 'ugly-app/*',
56
+ 'three', '@types/three', 'jsdom', 'gl',
57
+ '@ffmpeg-installer/ffmpeg', 'fluent-ffmpeg', 'sharp',
58
+ '@anthropic-ai/sdk', 'openai',
59
+ 'zod', 'date-fns', 'lodash',
60
+ 'mediasoup', 'mediasoup-client',
61
+ ];
62
+ const externalFlags = externals.map(e => `--external:${e}`).join(' ');
63
+ execSync(
64
+ `npx esbuild server/task.ts --bundle --platform=node --format=esm --outfile=dist/tasks/task.js ${externalFlags}`,
65
+ { stdio: 'inherit', cwd: process.cwd() },
66
+ );
67
+ console.log('[Build] Task bundle: dist/tasks/task.js');
68
+ }
69
+
47
70
  // Run tsc for typecheck
48
71
  console.log('[Build] Running tsc...');
49
72
  execSync('npx tsc --noEmit', { stdio: 'inherit', cwd: process.cwd() });
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by prebuild — do not edit manually
2
- export const CLI_VERSION = "0.1.240";
2
+ export const CLI_VERSION = "0.1.241";
@@ -0,0 +1,74 @@
1
+ /**
2
+ * TaskContext — the execution environment for project task bundles.
3
+ *
4
+ * Every project's `task.js` exports an `execute(ctx, data)` function.
5
+ * The platform creates a TaskContext scoped to the project's own DB and R2,
6
+ * then calls execute(). The project never needs DB credentials or R2 keys
7
+ * — the platform provides them through this interface.
8
+ *
9
+ * ```typescript
10
+ * // server/task.ts (project source)
11
+ * import type { TaskContext } from 'ugly-app/worker';
12
+ *
13
+ * export async function execute(ctx: TaskContext, data: { taskName: string }) {
14
+ * switch (data.taskName) {
15
+ * case 'dailyCleanup':
16
+ * const old = await ctx.db.find('todo', { done: true, updated: { $lt: daysAgo(30) } });
17
+ * for (const doc of old) await ctx.db.deleteDoc('todo', doc._id);
18
+ * break;
19
+ * }
20
+ * }
21
+ * ```
22
+ */
23
+ export interface TaskContext {
24
+ // ─── Database — project's isolated PostgreSQL ────────────────────────
25
+ db: TaskDB;
26
+
27
+ // ─── R2 storage — project's own bucket (ugly-{projectId}) ───────────
28
+ r2: TaskStorage;
29
+
30
+ // ─── Temp R2 — for large intermediate blobs (auto-cleaned) ──────────
31
+ tempR2: TaskTempStorage;
32
+
33
+ // ─── Task metadata ──────────────────────────────────────────────────
34
+ taskId: string;
35
+ taskName: string;
36
+ projectId: string;
37
+
38
+ // ─── For long-running tasks: extend ack deadline ────────────────────
39
+ working(): void;
40
+
41
+ // ─── Logging ────────────────────────────────────────────────────────
42
+ log(message: string, data?: Record<string, unknown>): void;
43
+ }
44
+
45
+ export interface TaskDB {
46
+ find(collection: string, query: Record<string, unknown>): Promise<Record<string, unknown>[]>;
47
+ getDoc(collection: string, id: string): Promise<Record<string, unknown> | null>;
48
+ setDoc(collection: string, doc: Record<string, unknown>): Promise<void>;
49
+ setDocFields(collection: string, id: string, fields: Record<string, unknown>): Promise<void>;
50
+ deleteDoc(collection: string, id: string): Promise<void>;
51
+ deleteMany(collection: string, ids: string[]): Promise<void>;
52
+ count(collection: string, query: Record<string, unknown>): Promise<number>;
53
+ }
54
+
55
+ export interface TaskStorage {
56
+ get(key: string): Promise<Buffer | null>;
57
+ getJSON<T = unknown>(key: string): Promise<T | null>;
58
+ put(key: string, data: Buffer | string, opts?: { contentType?: string }): Promise<string>;
59
+ delete(key: string): Promise<void>;
60
+ list(prefix: string): Promise<string[]>;
61
+ }
62
+
63
+ export interface TaskTempStorage {
64
+ get(key: string): Promise<Buffer | null>;
65
+ put(key: string, data: Buffer): Promise<string>;
66
+ }
67
+
68
+ /**
69
+ * The function signature that every project's task.js must export.
70
+ */
71
+ export type TaskExecuteFn = (
72
+ ctx: TaskContext,
73
+ data: Record<string, unknown>,
74
+ ) => Promise<unknown>;
@@ -0,0 +1,261 @@
1
+ import { createRequire } from 'module';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import type { TaskContext, TaskDB, TaskStorage, TaskTempStorage, TaskExecuteFn } from './TaskContext.js';
6
+
7
+ /* eslint-disable @typescript-eslint/no-explicit-any */
8
+
9
+ /**
10
+ * NPM packages that task bundles are allowed to install.
11
+ * Any package not on this list is rejected.
12
+ */
13
+ export const ALLOWED_PACKAGES = [
14
+ // Rendering
15
+ 'three', '@types/three', 'jsdom', 'gl',
16
+ // Video/Audio
17
+ '@ffmpeg-installer/ffmpeg', 'fluent-ffmpeg', 'sharp',
18
+ // AI
19
+ '@anthropic-ai/sdk', 'openai',
20
+ // Data
21
+ 'zod', 'date-fns', 'lodash',
22
+ // WebRTC
23
+ 'mediasoup', 'mediasoup-client',
24
+ // Framework
25
+ 'ugly-app',
26
+ ] as const;
27
+
28
+ const ALLOWED_SET = new Set<string>(ALLOWED_PACKAGES);
29
+
30
+ // ─── Warm process cache ────────────────────────────────────────────────────
31
+
32
+ interface CachedProcess {
33
+ codeUrl: string;
34
+ executeFn: TaskExecuteFn;
35
+ lastUsed: number;
36
+ workDir: string;
37
+ }
38
+
39
+ const processCache = new Map<string, CachedProcess>();
40
+ const IDLE_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
41
+
42
+ /**
43
+ * Configuration for the task host.
44
+ */
45
+ export interface TaskHostConfig {
46
+ /**
47
+ * Factory to create a scoped DB proxy for a project.
48
+ * The proxy restricts access to the project's own PostgreSQL user/database.
49
+ */
50
+ createDbProxy(projectId: string): TaskDB;
51
+
52
+ /**
53
+ * Factory to create a scoped R2 proxy for a project.
54
+ * Restricts access to the project's own R2 bucket.
55
+ */
56
+ createR2Proxy(projectId: string): TaskStorage;
57
+
58
+ /**
59
+ * Factory to create a temp R2 proxy for a project.
60
+ */
61
+ createTempR2Proxy(projectId: string): TaskTempStorage;
62
+
63
+ /**
64
+ * Fetch a task bundle from R2 by URL.
65
+ * Returns the JavaScript source code as a string.
66
+ */
67
+ fetchBundle(codeUrl: string): Promise<string>;
68
+
69
+ /** Working directory for task bundles. Default: os.tmpdir()/ugly-tasks/ */
70
+ workDir?: string;
71
+ }
72
+
73
+ /**
74
+ * TaskHost — loads project task.js bundles from R2, caches warm processes,
75
+ * and executes tasks with scoped DB/R2 access.
76
+ */
77
+ export class TaskHost {
78
+ private config: TaskHostConfig;
79
+ private cleanupTimer: ReturnType<typeof setInterval> | null = null;
80
+ private baseWorkDir: string;
81
+
82
+ constructor(config: TaskHostConfig) {
83
+ this.config = config;
84
+ this.baseWorkDir = config.workDir ?? path.join(os.tmpdir(), 'ugly-tasks');
85
+ fs.mkdirSync(this.baseWorkDir, { recursive: true });
86
+
87
+ // Periodic cleanup of idle processes
88
+ this.cleanupTimer = setInterval(() => this.cleanupIdle(), 60_000);
89
+ }
90
+
91
+ /**
92
+ * Execute a task using a cached or freshly loaded task bundle.
93
+ */
94
+ async execute(opts: {
95
+ codeUrl: string;
96
+ projectId: string;
97
+ taskId: string;
98
+ taskName: string;
99
+ data?: Record<string, unknown>;
100
+ timeoutMs?: number;
101
+ }): Promise<unknown> {
102
+ const { codeUrl, projectId, taskId, taskName, data = {} } = opts;
103
+
104
+ // Get or create warm process
105
+ const proc = await this.getOrCreateProcess(codeUrl);
106
+ proc.lastUsed = Date.now();
107
+
108
+ // Create scoped TaskContext
109
+ const ctx: TaskContext = {
110
+ db: this.config.createDbProxy(projectId),
111
+ r2: this.config.createR2Proxy(projectId),
112
+ tempR2: this.config.createTempR2Proxy(projectId),
113
+ taskId,
114
+ taskName,
115
+ projectId,
116
+ working: () => { /* extend deadline — platform manages this */ },
117
+ log: (message, logData) => {
118
+ console.log(`[Task:${projectId}/${taskName}] ${message}`, logData ?? '');
119
+ },
120
+ };
121
+
122
+ // Execute with timeout
123
+ const timeoutMs = opts.timeoutMs ?? 300_000; // 5 min default
124
+ const result = await Promise.race([
125
+ proc.executeFn(ctx, { taskName, ...data }),
126
+ new Promise((_, reject) =>
127
+ setTimeout(() => reject(new Error(`Task ${taskName} timed out after ${timeoutMs}ms`)), timeoutMs),
128
+ ),
129
+ ]);
130
+
131
+ return result;
132
+ }
133
+
134
+ /**
135
+ * Get a cached process for this code URL, or create one.
136
+ */
137
+ private async getOrCreateProcess(codeUrl: string): Promise<CachedProcess> {
138
+ const cached = processCache.get(codeUrl);
139
+ if (cached) return cached;
140
+
141
+ // Fetch bundle
142
+ const source = await this.config.fetchBundle(codeUrl);
143
+
144
+ // Create working directory for this bundle
145
+ const hash = codeUrl.replace(/[^a-zA-Z0-9]/g, '_').slice(-64);
146
+ const workDir = path.join(this.baseWorkDir, hash);
147
+ fs.mkdirSync(workDir, { recursive: true });
148
+
149
+ // Write bundle to disk
150
+ const bundlePath = path.join(workDir, 'task.mjs');
151
+ fs.writeFileSync(bundlePath, source, 'utf-8');
152
+
153
+ // Install allowlisted packages if package.json exists alongside bundle
154
+ const pkgJsonUrl = codeUrl.replace(/task\.js$/, 'package.json');
155
+ try {
156
+ const pkgSource = await this.config.fetchBundle(pkgJsonUrl);
157
+ const pkg = JSON.parse(pkgSource);
158
+ await this.installAllowedPackages(workDir, pkg);
159
+ } catch {
160
+ // No package.json — bundle is self-contained
161
+ }
162
+
163
+ // Dynamic import the module
164
+ const mod = await import(bundlePath) as { execute?: TaskExecuteFn };
165
+ if (typeof mod.execute !== 'function') {
166
+ throw new Error(`Task bundle at ${codeUrl} does not export an execute() function`);
167
+ }
168
+
169
+ const proc: CachedProcess = {
170
+ codeUrl,
171
+ executeFn: mod.execute,
172
+ lastUsed: Date.now(),
173
+ workDir,
174
+ };
175
+
176
+ processCache.set(codeUrl, proc);
177
+ console.log(`[TaskHost] Loaded bundle: ${codeUrl}`);
178
+ return proc;
179
+ }
180
+
181
+ /**
182
+ * Install only allowlisted packages from a package.json.
183
+ */
184
+ private async installAllowedPackages(workDir: string, pkg: { dependencies?: Record<string, string> }): Promise<void> {
185
+ if (!pkg.dependencies) return;
186
+
187
+ const filtered: Record<string, string> = {};
188
+ for (const [name, version] of Object.entries(pkg.dependencies)) {
189
+ if (ALLOWED_SET.has(name)) {
190
+ filtered[name] = version;
191
+ } else {
192
+ console.warn(`[TaskHost] Package "${name}" not in allowlist — skipped`);
193
+ }
194
+ }
195
+
196
+ if (Object.keys(filtered).length === 0) return;
197
+
198
+ // Write filtered package.json
199
+ fs.writeFileSync(
200
+ path.join(workDir, 'package.json'),
201
+ JSON.stringify({ dependencies: filtered }, null, 2),
202
+ );
203
+
204
+ // Install
205
+ const { execSync } = await import('child_process');
206
+ execSync('npm install --production --ignore-scripts', {
207
+ cwd: workDir,
208
+ stdio: 'pipe',
209
+ timeout: 120_000,
210
+ });
211
+
212
+ console.log(`[TaskHost] Installed ${Object.keys(filtered).length} allowlisted packages`);
213
+ }
214
+
215
+ /**
216
+ * Evict idle processes (no invocation for IDLE_TIMEOUT_MS).
217
+ */
218
+ private cleanupIdle(): void {
219
+ const now = Date.now();
220
+ for (const [url, proc] of processCache) {
221
+ if (now - proc.lastUsed > IDLE_TIMEOUT_MS) {
222
+ processCache.delete(url);
223
+ // Clean up work directory
224
+ try {
225
+ fs.rmSync(proc.workDir, { recursive: true, force: true });
226
+ } catch { /* ignore */ }
227
+ console.log(`[TaskHost] Evicted idle process: ${url}`);
228
+ }
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Invalidate a cached process (e.g., when buildId changes).
234
+ */
235
+ invalidate(codeUrl: string): void {
236
+ const proc = processCache.get(codeUrl);
237
+ if (proc) {
238
+ processCache.delete(codeUrl);
239
+ try {
240
+ fs.rmSync(proc.workDir, { recursive: true, force: true });
241
+ } catch { /* ignore */ }
242
+ console.log(`[TaskHost] Invalidated: ${codeUrl}`);
243
+ }
244
+ }
245
+
246
+ /**
247
+ * Shutdown — clean up all processes and timers.
248
+ */
249
+ async shutdown(): Promise<void> {
250
+ if (this.cleanupTimer) {
251
+ clearInterval(this.cleanupTimer);
252
+ this.cleanupTimer = null;
253
+ }
254
+ for (const [url, proc] of processCache) {
255
+ processCache.delete(url);
256
+ try {
257
+ fs.rmSync(proc.workDir, { recursive: true, force: true });
258
+ } catch { /* ignore */ }
259
+ }
260
+ }
261
+ }
@@ -0,0 +1,13 @@
1
+ export type {
2
+ TaskContext,
3
+ TaskDB,
4
+ TaskStorage,
5
+ TaskTempStorage,
6
+ TaskExecuteFn,
7
+ } from './TaskContext.js';
8
+
9
+ export {
10
+ TaskHost,
11
+ ALLOWED_PACKAGES,
12
+ type TaskHostConfig,
13
+ } from './TaskHost.js';