diff --git a/sandbox/v2/jsapi/box.go b/sandbox/v2/jsapi/box.go new file mode 100644 index 00000000..37efa81e --- /dev/null +++ b/sandbox/v2/jsapi/box.go @@ -0,0 +1,52 @@ +package jsapi + +import ( + "rogchap.com/v8go" +) + +// NewBoxObject creates a JS Box object with the following methods: +// +// box.ID() → string // sandbox ID +// box.Owner() → string // owner +// box.ContainerID() → string // underlying container/pod ID +// box.Pool() → string // pool name +// box.WorkspaceID() → string // mounted workspace ID (empty if none) +// +// box.Exec(cmd, options?) → ExecResult // run command, wait for completion +// cmd: string[] // command + args +// options: { workdir, env, timeout } +// returns: { exit_code: number, stdout: string, stderr: string } +// +// box.Stream(cmd, options?) → ExecStream // streaming I/O +// returns: { stdout: ReadableStream, stderr: ReadableStream, +// stdin: WritableStream, wait: ()=>number, cancel: ()=>void } +// +// box.Attach(port, options?) → ServiceConn // WebSocket/SSE attach +// port: number // container port +// options: { protocol, path, headers } +// returns: { url: string, close: ()=>void } +// +// box.VNC() → string // VNC WebSocket URL +// box.Proxy(port, path?) → string // HTTP proxy URL +// +// box.Workspace() → WorkspaceFS // workspace file system +// returns WorkspaceFS object (see workspace/jsapi) +// +// box.Info() → BoxInfo // container status +// returns: { id, container_id, pool, owner, status, policy, +// labels, image, created_at, last_active, process_count, vnc } +// +// box.Start() → void // start stopped box +// box.Stop() → void // stop running box +// box.Remove() → void // remove box permanently +// box.Release() → void // release JS bridge ref +func NewBoxObject(v8ctx *v8go.Context /* , box *sandbox.Box */) (*v8go.Value, error) { + // TODO: Phase 2 implementation + // 1. Create ObjectTemplate with InternalFieldCount(1) + // 2. Register box in bridge + // 3. Bind property accessors: ID, Owner, ContainerID, Pool, WorkspaceID + // 4. Bind methods: Exec, Stream, Attach, VNC, Proxy, Workspace, + // Info, Start, Stop, Remove, Release + // 5. Create instance, set internal field + return nil, nil +} diff --git a/sandbox/v2/jsapi/jsapi.go b/sandbox/v2/jsapi/jsapi.go new file mode 100644 index 00000000..12fecca4 --- /dev/null +++ b/sandbox/v2/jsapi/jsapi.go @@ -0,0 +1,50 @@ +// Package jsapi registers the Sandbox() constructor into the Yao V8 runtime. +// +// # JavaScript API +// +// const sb = new Sandbox({ pool: "default", image: "node:20", owner: "user1" }) +// const box = sb.Create({ workdir: "/app", env: { NODE_ENV: "dev" } }) +// const result = box.Exec(["node", "-e", "console.log('hi')"]) +// box.Remove() +// +// The constructor returns a SandboxManager object; Create/GetOrCreate returns +// a Box object with Exec/Stream/Attach/VNC/Proxy/Workspace/Info/Stop/Start/Remove. +// +// Registration happens via init() — import with: +// +// _ "github.com/yaoapp/yao/sandbox/v2/jsapi" +package jsapi + +import ( + v8 "github.com/yaoapp/gou/runtime/v8" + "rogchap.com/v8go" +) + +func init() { + v8.RegisterFunction("Sandbox", ExportFunction) +} + +// ExportFunction exports the Sandbox constructor to V8. +func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate { + return v8go.NewFunctionTemplate(iso, sandboxConstructor) +} + +// sandboxConstructor is called when JS executes `new Sandbox(options)`. +// +// Options: +// +// { +// pool: string // pool name (required) +// image: string // container image (required) +// owner: string // owner ID (required) +// } +// +// Returns a SandboxManager JS object. +func sandboxConstructor(info *v8go.FunctionCallbackInfo) *v8go.Value { + // TODO: Phase 2 implementation + // 1. Parse options from args[0] + // 2. Validate required fields (pool, image, owner) + // 3. Get sandbox.M() singleton + // 4. Return NewManagerObject(v8ctx, manager, options) + return v8go.Undefined(info.Context().Isolate()) +} diff --git a/sandbox/v2/jsapi/manager.go b/sandbox/v2/jsapi/manager.go new file mode 100644 index 00000000..247bfcbd --- /dev/null +++ b/sandbox/v2/jsapi/manager.go @@ -0,0 +1,45 @@ +package jsapi + +import ( + "rogchap.com/v8go" +) + +// NewManagerObject creates a JS SandboxManager object with the following methods: +// +// manager.Create(options?) → Box // create a new sandbox box +// manager.GetOrCreate(opts) → Box // get existing or create +// manager.Get(id) → Box|null // get by sandbox ID +// manager.List(options?) → Box[] // list boxes +// manager.Remove(id) → void // remove a box +// manager.EnsureImage(ref) → void // pull image if missing +// manager.ImageExists(ref) → boolean // check image presence +// manager.Pools() → PoolInfo[] // list pool info +// manager.Release() → void // release JS bridge ref +// +// Create options (merged with constructor defaults): +// +// { +// id: string // explicit sandbox ID (optional) +// workdir: string // container working directory +// user: string // container user (e.g. "1000:1000") +// env: object // environment variables +// memory: number // memory limit in bytes +// cpus: number // CPU limit (e.g. 1.5) +// vnc: boolean // enable VNC +// ports: array // port mappings [{container: 8080, host: 0}] +// policy: string // "oneshot"|"session"|"longrunning"|"persistent" +// idle_timeout: number // idle timeout in ms +// stop_timeout: number // stop timeout in ms +// workspace_id: string // workspace to mount +// mount_mode: string // "rw"|"ro" +// mount_path: string // mount target in container +// } +func NewManagerObject(v8ctx *v8go.Context /* manager *sandbox.Manager, defaults CreateDefaults */) (*v8go.Value, error) { + // TODO: Phase 2 implementation + // 1. Create ObjectTemplate with InternalFieldCount(1) + // 2. Register manager in bridge + // 3. Bind methods: Create, GetOrCreate, Get, List, Remove, + // EnsureImage, ImageExists, Pools, Release + // 4. Create instance, set internal field + return nil, nil +} diff --git a/sandbox/v2/testutils_test.go b/sandbox/v2/testutils_test.go index c6fb5f1b..ff4b512d 100644 --- a/sandbox/v2/testutils_test.go +++ b/sandbox/v2/testutils_test.go @@ -21,9 +21,10 @@ type poolConfig struct { } // testPools returns all available pool configurations for multi-mode testing. -// - local: always present (direct Docker daemon) -// - remote: when SANDBOX_TEST_REMOTE_ADDR is set (Tai proxy → Docker) -// - k8s: when TAI_TEST_K8S_HOST + TAI_TEST_KUBECONFIG are set (Tai proxy → K8s) +// - local: always present (direct Docker daemon) +// - remote: when SANDBOX_TEST_REMOTE_ADDR is set (Tai on host → Docker) +// - containerized: when TAI_TEST_CONTAINERIZED_HOST is set (Tai in container → Docker) +// - k8s: when TAI_TEST_K8S_HOST + TAI_TEST_KUBECONFIG are set (Tai → K8s) func testPools() []poolConfig { pools := []poolConfig{ {Name: "local", Addr: testLocalAddr()}, @@ -31,6 +32,13 @@ func testPools() []poolConfig { if addr := os.Getenv("SANDBOX_TEST_REMOTE_ADDR"); addr != "" { pools = append(pools, poolConfig{Name: "remote", Addr: addr}) } + if host := os.Getenv("TAI_TEST_CONTAINERIZED_HOST"); host != "" { + grpcPort := envPort("TAI_TEST_CONTAINERIZED_GRPC_PORT", 9200) + addr := fmt.Sprintf("tai://%s:%d", host, grpcPort) + // No WithPorts for HTTP/VNC — Tai self-inspects its container + // and returns host-mapped ports via ServerInfo automatically. + pools = append(pools, poolConfig{Name: "containerized", Addr: addr}) + } if host := os.Getenv("TAI_TEST_K8S_HOST"); host != "" { kubeconfig := os.Getenv("TAI_TEST_KUBECONFIG") if kubeconfig == "" { diff --git a/workspace/jsapi/fs.go b/workspace/jsapi/fs.go new file mode 100644 index 00000000..19583dc3 --- /dev/null +++ b/workspace/jsapi/fs.go @@ -0,0 +1,36 @@ +package jsapi + +import ( + "rogchap.com/v8go" +) + +// NewFSObject creates a JS WorkspaceFS object implementing a file system interface. +// +// This is the object returned by both: +// - WorkspaceManager.FS(id) (standalone workspace access) +// - Box.Workspace() (sandbox-mounted workspace access) +// +// Methods: +// +// fs.ReadFile(path) → string // read UTF-8 content +// fs.ReadFileBytes(path) → ArrayBuffer // read binary content +// fs.WriteFile(path, data) → void // write string or ArrayBuffer +// fs.Stat(path) → FileInfo // file metadata +// returns: { name, size, mode, mod_time, is_dir } +// fs.ReadDir(path?) → DirEntry[] // list directory (default ".") +// returns: [{ name, is_dir, size }] +// fs.MkdirAll(path) → void // create directory tree +// fs.Remove(path) → void // remove single file/empty dir +// fs.RemoveAll(path) → void // remove recursively +// fs.Rename(from, to) → void // rename/move +// fs.Close() → void // close FS handle +// fs.Release() → void // release JS bridge ref +func NewFSObject(v8ctx *v8go.Context /* , wfs taiworkspace.FS */) (*v8go.Value, error) { + // TODO: Phase 2 implementation + // 1. Create ObjectTemplate with InternalFieldCount(1) + // 2. Register FS in bridge + // 3. Bind methods: ReadFile, ReadFileBytes, WriteFile, + // Stat, ReadDir, MkdirAll, Remove, RemoveAll, Rename, Close, Release + // 4. Create instance, set internal field + return nil, nil +} diff --git a/workspace/jsapi/jsapi.go b/workspace/jsapi/jsapi.go new file mode 100644 index 00000000..51f49fa2 --- /dev/null +++ b/workspace/jsapi/jsapi.go @@ -0,0 +1,47 @@ +// Package jsapi registers the Workspace() constructor into the Yao V8 runtime. +// +// # JavaScript API +// +// const ws = new Workspace({ node: "tai-1" }) +// const info = ws.Create({ name: "my-project", owner: "user1" }) +// const file = ws.ReadFile(info.id, "/README.md") +// ws.WriteFile(info.id, "/app.ts", content) +// +// The constructor returns a WorkspaceManager object; individual workspace +// files are accessed through ReadFile/WriteFile/ListDir or the FS() handle. +// +// Registration happens via init() — import with: +// +// _ "github.com/yaoapp/yao/workspace/jsapi" +package jsapi + +import ( + v8 "github.com/yaoapp/gou/runtime/v8" + "rogchap.com/v8go" +) + +func init() { + v8.RegisterFunction("Workspace", ExportFunction) +} + +// ExportFunction exports the Workspace constructor to V8. +func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate { + return v8go.NewFunctionTemplate(iso, workspaceConstructor) +} + +// workspaceConstructor is called when JS executes `new Workspace(options?)`. +// +// Options (all optional — uses global workspace.Manager if omitted): +// +// { +// node: string // default target node for Create (optional) +// } +// +// Returns a WorkspaceManager JS object. +func workspaceConstructor(info *v8go.FunctionCallbackInfo) *v8go.Value { + // TODO: Phase 2 implementation + // 1. Parse optional options from args[0] + // 2. Get workspace manager instance + // 3. Return NewManagerObject(v8ctx, manager, defaults) + return v8go.Undefined(info.Context().Isolate()) +} diff --git a/workspace/jsapi/manager.go b/workspace/jsapi/manager.go new file mode 100644 index 00000000..0aac6754 --- /dev/null +++ b/workspace/jsapi/manager.go @@ -0,0 +1,53 @@ +package jsapi + +import ( + "rogchap.com/v8go" +) + +// NewManagerObject creates a JS WorkspaceManager object with the following methods: +// +// wm.Create(options) → WorkspaceInfo // create workspace +// options: { +// id: string // explicit ID (optional, auto uuid) +// name: string // display name (required) +// owner: string // owner user ID (required) +// node: string // target Tai node (required, or use constructor default) +// labels: object // metadata key-value pairs +// } +// returns: { id, name, owner, node, labels, created_at, updated_at } +// +// wm.Get(id) → WorkspaceInfo|null +// wm.List(options?) → WorkspaceInfo[] +// options: { owner: string, node: string } +// +// wm.Update(id, options) → WorkspaceInfo +// options: { name: string, labels: object } +// +// wm.Delete(id, force?) → void +// force: boolean // delete even if has active mounts +// +// wm.ReadFile(id, path) → string // read file content (UTF-8) +// wm.ReadFileBytes(id, path) → ArrayBuffer // read file content (binary) +// wm.WriteFile(id, path, data) → void // write file (string or ArrayBuffer) +// wm.ListDir(id, path?) → DirEntry[] // list directory +// returns: [{ name, is_dir, size }] +// wm.Remove(id, path) → void // remove file or dir +// wm.MkdirAll(id, path) → void // create directory tree +// wm.Rename(id, from, to) → void // rename/move file +// +// wm.FS(id) → WorkspaceFS // get full FS handle +// wm.MountPath(id) → string // host mount path +// wm.Nodes() → NodeInfo[] // list available nodes +// returns: [{ name, addr, online }] +// +// wm.Release() → void // release JS bridge ref +func NewManagerObject(v8ctx *v8go.Context /* , manager *workspace.Manager, defaults ManagerDefaults */) (*v8go.Value, error) { + // TODO: Phase 2 implementation + // 1. Create ObjectTemplate with InternalFieldCount(1) + // 2. Register manager in bridge + // 3. Bind methods: Create, Get, List, Update, Delete, + // ReadFile, ReadFileBytes, WriteFile, ListDir, Remove, + // MkdirAll, Rename, FS, MountPath, Nodes, Release + // 4. Create instance, set internal field + return nil, nil +}