diff --git a/src/puter-js/README.md b/src/puter-js/README.md index f314ede0..8167449c 100644 --- a/src/puter-js/README.md +++ b/src/puter-js/README.md @@ -15,7 +15,7 @@ npm install @heyputer/puter.js #### ES Modules ```js -import puter from '@heyputer/puterjs'; +import '@heyputer/puter.js'; ``` #### CommonJS @@ -34,6 +34,8 @@ Include Puter.js directly in your HTML via CDN in the `` section: ## Usage Example +After importing, you can use the global `puter` object: + ```js // Print a message puter.print('Hello from Puter.js!'); diff --git a/src/puter-js/index.d.ts b/src/puter-js/index.d.ts new file mode 100644 index 00000000..72c452cc --- /dev/null +++ b/src/puter-js/index.d.ts @@ -0,0 +1,479 @@ +declare global { + interface Window { + puter: Puter; + } +} + +declare namespace Puter { + // Main Puter interface + interface Puter { + // Properties + appID: string; + env: 'app' | 'web' | 'gui'; + + // Utility methods + print(text: string, options?: { code?: boolean }): void; + randName(separator?: string): string; + exit(statusCode?: number): void; + + // Sub-modules + ai: AI; + apps: Apps; + auth: Auth; + drivers: Drivers; + fs: FileSystem; + hosting: Hosting; + kv: KeyValue; + net: Networking; + perms: Permissions; + ui: UI; + workers: Workers; + } + + // AI Module + interface AI { + chat(prompt: string, options?: ChatOptions): Promise; + chat(prompt: string, testMode?: boolean, options?: ChatOptions): Promise; + chat(prompt: string, imageURL?: string, testMode?: boolean, options?: ChatOptions): Promise; + chat(prompt: string, imageURLArray?: string[], testMode?: boolean, options?: ChatOptions): Promise; + chat(messages: ChatMessage[], testMode?: boolean, options?: ChatOptions): Promise; + + img2txt(image: string | File | Blob, testMode?: boolean): Promise; + + txt2img(prompt: string, testMode?: boolean): Promise; + txt2img(prompt: string, options?: Txt2ImgOptions): Promise; + + txt2speech(text: string): Promise; + txt2speech(text: string, options?: Txt2SpeechOptions): Promise; + txt2speech(text: string, language?: string): Promise; + txt2speech(text: string, language?: string, voice?: string): Promise; + txt2speech(text: string, language?: string, voice?: string, engine?: string): Promise; + } + + interface ChatOptions { + model?: string; + stream?: boolean; + max_tokens?: number; + temperature?: number; + tools?: ToolDefinition[]; + } + + interface ToolDefinition { + type: 'function'; + function: { + name: string; + description: string; + parameters: object; + strict?: boolean; + }; + } + + interface ChatMessage { + role: 'system' | 'assistant' | 'user' | 'function' | 'tool'; + content: string | ContentObject[]; + tool_call_id?: string; + } + + interface ContentObject { + type: 'text' | 'file'; + text?: string; + puter_path?: string; + } + + interface ChatResponse { + message: { + role: string; + content: string; + tool_calls?: ToolCall[]; + }; + } + + interface ToolCall { + id: string; + function: { + name: string; + arguments: string; + }; + } + + interface Txt2ImgOptions { + model?: 'gpt-image-1' | 'gemini-2.5-flash-image-preview' | 'dall-e-3'; + quality?: 'high' | 'medium' | 'low' | 'hd' | 'standard'; + input_image?: string; + input_image_mime_type?: string; + } + + interface Txt2SpeechOptions { + language?: string; + voice?: string; + engine?: 'standard' | 'neural' | 'generative'; + } + + // Apps Module + interface Apps { + create(name: string, indexURL: string): Promise; + create(name: string, indexURL: string, title?: string): Promise; + create(name: string, indexURL: string, title?: string, description?: string): Promise; + create(options: CreateAppOptions): Promise; + + delete(name: string): Promise; + get(name: string, options?: GetAppOptions): Promise; + list(options?: ListAppOptions): Promise; + update(name: string, attributes: UpdateAppAttributes): Promise; + } + + interface CreateAppOptions { + name: string; + indexURL: string; + title?: string; + description?: string; + icon?: string; + maximizeOnStart?: boolean; + filetypeAssociations?: string[]; + } + + interface GetAppOptions { + stats_period?: StatsPeriod; + icon_size?: null | 16 | 32 | 64 | 128 | 256 | 512; + } + + interface ListAppOptions extends GetAppOptions { } + + interface UpdateAppAttributes { + name?: string; + indexURL?: string; + title?: string; + description?: string; + icon?: string; + maximizeOnStart?: boolean; + filetypeAssociations?: string[]; + } + + type StatsPeriod = 'all' | 'today' | 'yesterday' | '7d' | '30d' | 'this_month' | 'last_month' | 'this_year' | 'last_year' | 'month_to_date' | 'year_to_date' | 'last_12_months'; + + interface App { + uid: string; + name: string; + icon: string; + description: string; + title: string; + maximize_on_start: boolean; + index_url: string; + created_at: string; + background: boolean; + filetype_associations: string[]; + open_count: number; + user_count: number; + } + + // Auth Module + interface Auth { + signIn(options?: { attempt_temp_user_creation?: boolean }): Promise; + signOut(): void; + isSignedIn(): boolean; + getUser(): Promise; + } + + interface User { + uuid: string; + username: string; + email_confirmed: boolean; + } + + // Drivers Module + interface Drivers { + call(interface: string, driver: string, method: string, args?: object): Promise; + } + + // FileSystem Module + interface FileSystem { + copy(source: string, destination: string, options?: CopyOptions): Promise; + delete(path: string, options?: DeleteOptions): Promise; + getReadURL(path: string, expiresIn?: number): Promise; + mkdir(path: string, options?: MkdirOptions): Promise; + move(source: string, destination: string, options?: MoveOptions): Promise; + read(path: string, options?: ReadOptions): Promise; + readdir(path: string, options?: ReaddirOptions): Promise; + readdir(options?: ReaddirOptions): Promise; + rename(path: string, newName: string): Promise; + space(): Promise; + stat(path: string): Promise; + upload(items: FileList | File[] | Blob[], dirPath?: string, options?: object): Promise; + write(path: string, data?: string | File | Blob, options?: WriteOptions): Promise; + } + + interface CopyOptions { + overwrite?: boolean; + dedupeName?: boolean; + newName?: string; + } + + interface DeleteOptions { + recursive?: boolean; + descendantsOnly?: boolean; + } + + interface MkdirOptions { + overwrite?: boolean; + dedupeName?: boolean; + createMissingParents?: boolean; + } + + interface MoveOptions extends CopyOptions { + createMissingParents?: boolean; + } + + interface ReadOptions { + offset?: number; + byte_count?: number; + } + + interface ReaddirOptions { + path?: string; + uid?: string; + } + + interface WriteOptions { + overwrite?: boolean; + dedupeName?: boolean; + createMissingParents?: boolean; + } + + interface SpaceInfo { + capacity: number; + used: number; + } + + interface FSItem { + id: string; + uid: string; + name: string; + path: string; + is_dir: boolean; + parent_id: string; + parent_uid: string; + created: number; + modified: number; + accessed: number; + size: number | null; + writable: boolean; + read(): Promise; + readdir(): Promise; + } + + // Hosting Module + interface Hosting { + create(subdomain: string, dirPath?: string): Promise; + delete(subdomain: string): Promise; + get(subdomain: string): Promise; + list(): Promise; + update(subdomain: string, dirPath?: string): Promise; + } + + interface Subdomain { + uid: string; + subdomain: string; + root_dir: FSItem; + } + + // KeyValue Module + interface KeyValue { + set(key: string, value: string | number | boolean | object | any[]): Promise; + get(key: string): Promise; + del(key: string): Promise; + incr(key: string, amount?: number): Promise; + decr(key: string, amount?: number): Promise; + list(pattern?: string, returnValues?: boolean): Promise; + list(returnValues?: boolean): Promise; + flush(): Promise; + } + + interface KeyValuePair { + key: string; + value: any; + } + + // Networking Module + interface Networking { + fetch(url: string, options?: RequestInit): Promise; + Socket: typeof Socket; + tls: { + TLSSocket: typeof TLSSocket; + }; + } + + class Socket { + constructor(hostname: string, port: number); + write(data: ArrayBuffer | Uint8Array | string): void; + close(): void; + on(event: 'open', callback: () => void): void; + on(event: 'data', callback: (buffer: Uint8Array) => void): void; + on(event: 'error', callback: (reason: string) => void): void; + on(event: 'close', callback: (hadError: boolean) => void): void; + } + + class TLSSocket extends Socket { + constructor(hostname: string, port: number); + } + + // Permissions Module + interface Permissions { + grantApp(app_uid: string, permissionString: string): Promise; + grantAppAnyUser(app_uid: string, permissionString: string): Promise; + grantGroup(group_uid: string, permissionString: string): Promise; + grantOrigin(origin: string, permissionString: string): Promise; + grantUser(username: string, permissionString: string): Promise; + revokeApp(app_uid: string, permissionString: string): Promise; + revokeAppAnyUser(app_uid: string, permissionString: string): Promise; + revokeGroup(group_uid: string, permissionString: string): Promise; + revokeOrigin(origin: string, permissionString: string): Promise; + revokeUser(username: string, permissionString: string): Promise; + } + + // UI Module + interface UI { + alert(message?: string, buttons?: AlertButton[]): Promise; + prompt(message?: string, defaultValue?: string): Promise; + authenticateWithPuter(): Promise; + contextMenu(options: ContextMenuOptions): void; + createWindow(options?: WindowOptions): void; + exit(statusCode?: number): void; + getLanguage(): Promise; + hideSpinner(): void; + launchApp(appName?: string, args?: object): Promise; + launchApp(options: LaunchAppOptions): Promise; + on(eventName: 'localeChanged', handler: (data: { language: string }) => void): void; + on(eventName: 'themeChanged', handler: (data: ThemeData) => void): void; + onItemsOpened(handler: (items: FSItem[]) => void): void; + onLaunchedWithItems(handler: (items: FSItem[]) => void): void; + onWindowClose(handler: () => void): void; + parentApp(): AppConnection | null; + setMenubar(options: MenubarOptions): void; + setWindowHeight(height: number): void; + setWindowPosition(x: number, y: number): void; + setWindowSize(width: number, height: number): void; + setWindowTitle(title: string): void; + setWindowWidth(width: number): void; + setWindowX(x: number): void; + setWindowY(y: number): void; + showColorPicker(defaultColor?: string): Promise; + showColorPicker(options?: object): Promise; + showDirectoryPicker(options?: { multiple?: boolean }): Promise; + showFontPicker(defaultFont?: string): Promise<{ fontFamily: string }>; + showFontPicker(options?: object): Promise<{ fontFamily: string }>; + showOpenFilePicker(options?: FilePickerOptions): Promise; + showSaveFilePicker(data?: any, defaultFileName?: string): Promise; + showSpinner(): void; + socialShare(url: string, message?: string, options?: { left?: number; top?: number }): void; + wasLaunchedWithItems(): boolean; + } + + interface AlertButton { + label: string; + value?: string; + type?: 'primary' | 'success' | 'info' | 'warning' | 'danger'; + } + + interface ContextMenuOptions { + items: (ContextMenuItem | '-')[]; + } + + interface ContextMenuItem { + label: string; + action?: () => void; + icon?: string; + icon_active?: string; + disabled?: boolean; + items?: (ContextMenuItem | '-')[]; + } + + interface WindowOptions { + center?: boolean; + content?: string; + disable_parent_window?: boolean; + has_head?: boolean; + height?: number; + is_resizable?: boolean; + show_in_taskbar?: boolean; + title?: string; + width?: number; + } + + interface LaunchAppOptions { + name?: string; + args?: object; + } + + interface ThemeData { + palette: { + primaryHue: number; + primarySaturation: string; + primaryLightness: string; + primaryAlpha: number; + primaryColor: string; + }; + } + + interface MenubarOptions { + items: MenuItem[]; + } + + interface MenuItem { + label: string; + action?: () => void; + items?: MenuItem[]; + } + + interface FilePickerOptions { + multiple?: boolean; + accept?: string | string[]; + } + + interface AppConnection { + usesSDK: boolean; + on(eventName: 'message', handler: (message: any) => void): void; + on(eventName: 'close', handler: (data: { appInstanceID: string }) => void): void; + off(eventName: string, handler: Function): void; + postMessage(message: any): void; + close(): void; + } + + // Workers Module + interface Workers { + create(workerName: string, filePath: string): Promise; + delete(workerName: string): Promise; + exec(workerURL: string, options?: WorkerExecOptions): Promise; + get(workerName: string): Promise; + list(): Promise; + } + + interface WorkerDeployment { + success: boolean; + url: string; + errors: any[]; + } + + interface WorkerExecOptions extends RequestInit { + method?: string; + headers?: object; + body?: string | object; + cache?: RequestCache; + credentials?: RequestCredentials; + mode?: RequestMode; + redirect?: RequestRedirect; + referrer?: string; + signal?: AbortSignal; + } + + interface WorkerInfo { + name: string; + url: string; + file_path: string; + file_uid: string; + created_at: string; + } +} + +declare const puter: Puter.Puter; + +export = Puter; \ No newline at end of file diff --git a/src/puter-js/package.json b/src/puter-js/package.json index 3b98559a..ba0a01c8 100644 --- a/src/puter-js/package.json +++ b/src/puter-js/package.json @@ -2,7 +2,13 @@ "name": "@heyputer/puterjs", "version": "2.0.0", "description": "Puter.js - A JavaScript library for interacting with Puter services.", - "main": "index.js", + "main": "src/index.js", + "types": "index.d.ts", + "typings": "index.d.ts", + "files": [ + "src/index.js", + "index.d.ts" + ], "type": "module", "publishConfig": { "registry": "https://registry.npmjs.org/" @@ -11,13 +17,17 @@ "type": "git", "url": "git+https://github.com/HeyPuter/puter.git" }, + "keywords": [ + "puter", + "puter.js", + "puterjs" + ], "scripts": { "start-server": "npx http-server --cors -c-1", "start-webpack": "webpack && webpack --output-filename puter.dev.js --watch --devtool source-map", "start": "concurrently \"npm run start-server\" \"npm run start-webpack\"", "build": "webpack && { echo \"// Copyright 2024-present Puter Technologies Inc. All rights reserved.\"; echo \"// Generated on $(date '+%Y-%m-%d %H:%M')\n\"; cat ./dist/puter.js; } > temp && mv temp ./dist/puter.js" }, - "keywords": [], "author": "Puter Technologies Inc.", "license": "Apache-2.0", "devDependencies": { @@ -25,6 +35,7 @@ "webpack-cli": "^5.1.4" }, "dependencies": { - "@heyputer/kv.js": "^0.1.92" + "@heyputer/kv.js": "^0.1.92", + "@heyputer/putility": "^1.0.3" } } \ No newline at end of file diff --git a/src/puter-js/webpack.config.js b/src/puter-js/webpack.config.js index 1eb4bd18..bacc727b 100644 --- a/src/puter-js/webpack.config.js +++ b/src/puter-js/webpack.config.js @@ -11,15 +11,15 @@ import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default { - entry: './src/index.js', - output: { - filename: 'puter.js', - path: path.resolve(__dirname, 'dist'), - }, - plugins: [ - new webpack.DefinePlugin({ - 'globalThis.PUTER_ORIGIN': JSON.stringify(process.env.PUTER_ORIGIN || 'https://puter.com'), - 'globalThis.PUTER_API_ORIGIN': JSON.stringify(process.env.PUTER_API_ORIGIN || 'https://api.puter.com'), - }), - ], + entry: './src/index.js', + output: { + filename: 'puter.js', + path: path.resolve(__dirname, 'dist'), + }, + plugins: [ + new webpack.DefinePlugin({ + 'globalThis.PUTER_ORIGIN': JSON.stringify(process.env.PUTER_ORIGIN || 'https://puter.com'), + 'globalThis.PUTER_API_ORIGIN': JSON.stringify(process.env.PUTER_API_ORIGIN || 'https://api.puter.com'), + }), + ], }; diff --git a/src/putility/package.json b/src/putility/package.json index 8be27008..ec2eda38 100644 --- a/src/putility/package.json +++ b/src/putility/package.json @@ -1,6 +1,6 @@ { "name": "@heyputer/putility", - "version": "1.0.2", + "version": "1.0.3", "description": "", "main": "index.js", "scripts": { @@ -10,4 +10,4 @@ }, "author": "Puter Technologies Inc.", "license": "AGPL-3.0-only" -} +} \ No newline at end of file diff --git a/src/putility/src/system/ServiceManager.js b/src/putility/src/system/ServiceManager.js index 038ff4bf..8cf24b92 100644 --- a/src/putility/src/system/ServiceManager.js +++ b/src/putility/src/system/ServiceManager.js @@ -1,60 +1,33 @@ /* * Copyright (C) 2024-present Puter Technologies Inc. - * + * * This file is part of Puter. - * + * * Puter is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. - * + * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -const { AdvancedBase } = require("../AdvancedBase"); -const { TService } = require("../concepts/Service"); -const { TeePromise } = require("../libs/promise"); - -const mkstatus = name => { - const c = class { - get label () { return name } - describe () { return name } - } - c.name = `Status${ - name[0].toUpperCase() + name.slice(1) - }` - return c; -} +const { AdvancedBase } = require('../AdvancedBase'); +const { TService } = require('../concepts/Service'); +const StatusEnum = { + Registering: 'registering', + Pending: 'pending', + Initializing: 'initializing', + Running: 'running', +}; class ServiceManager extends AdvancedBase { - static StatusRegistering = mkstatus('registering'); - static StatusPending = class StatusPending { - constructor ({ waiting_for }) { - this.waiting_for = waiting_for; - } - get label () { return 'waiting'; } - // TODO: trait? - describe () { - return `waiting for: ${this.waiting_for.join(', ')}` - } - } - static StatusInitializing = mkstatus('initializing'); - static StatusRunning = class StatusRunning { - constructor ({ start_ts }) { - this.start_ts = start_ts; - } - get label () { return 'running'; } - describe () { - return `running (since ${this.start_ts})`; - } - } - constructor ({ context } = {}) { + constructor({ context } = {}) { super(); this.context = context; @@ -68,7 +41,7 @@ class ServiceManager extends AdvancedBase { // initialized; mapped like: waiting_[dependency] = Set(dependents) this.waiting_ = {}; } - async register (name, factory, options = {}) { + async register(name, factory, options = {}) { await new Promise(rslv => setTimeout(rslv, 0)); const ins = factory.create({ @@ -78,25 +51,25 @@ class ServiceManager extends AdvancedBase { const entry = { name, instance: ins, - status: new this.constructor.StatusRegistering(), + status: StatusEnum.Registering, }; this.services_l_.push(entry); this.services_m_[name] = entry; await this.maybe_init_(name); } - info (name) { + info(name) { return this.services_m_[name]; } - get (name) { + get(name) { const info = this.services_m_[name]; if ( ! info ) throw new Error(`Service not registered: ${name}`); - if ( ! (info.status instanceof this.constructor.StatusRunning ) ) { + if ( info.status !== StatusEnum.Running ) { return undefined; } return info.instance; } - async aget (name) { + async aget(name) { await this.wait_for_init([name]); return this.get(name); } @@ -105,7 +78,7 @@ class ServiceManager extends AdvancedBase { * Wait for the specified list of services to be initialized. * @param {*} depends - list of services to wait for */ - async wait_for_init (depends) { + async wait_for_init(depends) { let check; await new Promise(rslv => { @@ -134,7 +107,7 @@ class ServiceManager extends AdvancedBase { }); }; - get_waiting_for_ (depends) { + get_waiting_for_(depends) { const waiting_for = []; for ( const depend of depends ) { const depend_entry = this.services_m_[depend]; @@ -142,14 +115,14 @@ class ServiceManager extends AdvancedBase { waiting_for.push(depend); continue; } - if ( ! (depend_entry.status instanceof this.constructor.StatusRunning) ) { + if ( ( depend_entry.status !== StatusEnum.Running ) ) { waiting_for.push(depend); } } return waiting_for; } - async maybe_init_ (name) { + async maybe_init_(name) { const entry = this.services_m_[name]; const depends = entry.instance.as(TService).get_depends(); const waiting_for = this.get_waiting_for_(depends); @@ -160,34 +133,33 @@ class ServiceManager extends AdvancedBase { } for ( const dependency of waiting_for ) { - /** @type Set */ - const waiting_set = this.waiting_[dependency] || - (this.waiting_[dependency] = new Set()); - waiting_set.add(name); + if ( !this.waiting_[dependency] ) { + this.waiting_[dependency] = new Set(); + } + this.waiting_[dependency].add(name); } - entry.status = new this.constructor.StatusPending( - { waiting_for }); + entry.status = StatusEnum.Pending; + entry.statusWaitingFor = waiting_for; } // called when a service has all of its dependencies initialized // and is ready to be initialized itself - async init_service_ (name, modifiers = {}) { + async init_service_(name, modifiers = {}) { const entry = this.services_m_[name]; - entry.status = new this.constructor.StatusInitializing(); + entry.status = StatusEnum.Initializing; const service_impl = entry.instance.as(TService); await service_impl.init(); - entry.status = new this.constructor.StatusRunning({ - start_ts: new Date(), - }); + entry.status = StatusEnum.Running; + entry.statusStartTS = new Date(); /** @type Set */ const maybe_ready_set = this.waiting_[name]; const promises = []; if ( maybe_ready_set ) { for ( const dependent of maybe_ready_set.values() ) { promises.push(this.maybe_init_(dependent, { - no_init_listeners: true + no_init_listeners: true, })); } }