diff --git a/README.md b/README.md index ab84dd4..2706958 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,25 @@ const output = await client.commands.run("echo 'Hello World'"); console.log(output); // Hello World ``` +## Running tests + +### All tests + +- Run all tests with `npm run test` +- Run specific test file `npm run test -- filesystem` + +### E2E production + +- Run e2e tests with `npm run test:e2e` +- Run specific test file `npm run test -- filesystem` + +### E2E local + +- Clone the sandbox templates repo (https://github.com/codesandbox/sandbox-templates) +- Build template with `csb build ../sandbox-templates/nextjs` +- Run e2e tests with `CSB_BASE_URL=https://api.codesandbox.dev CSB_TEMPLATE_ID=$NEXTJS_TEMPLATE_ID npm run test:e2e` +- Run specific test file `CSB_BASE_URL=https://api.codesandbox.dev CSB_TEMPLATE_ID=$NEXTJS_TEMPLATE_ID npm run test -- filesystem` + ## Efficient Sandbox Retrieval When you need to retrieve metadata for specific sandboxes by their IDs, you can use the efficient retrieval methods instead of listing and filtering all sandboxes: diff --git a/openapi-git-spec.json b/openapi-git-spec.json deleted file mode 100644 index 3a8d660..0000000 --- a/openapi-git-spec.json +++ /dev/null @@ -1,1348 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Git API", - "description": "API for interacting with Git version control in sandboxes", - "version": "1.0.0" - }, - "paths": { - "/git/status": { - "post": { - "summary": "Get git status", - "description": "Retrieve the current git status of the repository", - "operationId": "gitStatus", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/GitStatus" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving git status", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/remotes": { - "post": { - "summary": "Get git remotes", - "description": "Retrieve the remote repositories configured for the git repository", - "operationId": "gitRemotes", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/GitRemotes" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving git remotes", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/targetDiff": { - "post": { - "summary": "Get target diff", - "description": "Retrieve the difference between the current branch and a target branch", - "operationId": "gitTargetDiff", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "branch": { - "type": "string", - "description": "Target branch name" - } - }, - "required": ["branch"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/GitTargetDiff" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving target diff", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/pull": { - "post": { - "summary": "Pull changes", - "description": "Pull changes from the remote repository", - "operationId": "gitPull", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "branch": { - "type": "string", - "description": "Branch to pull from" - }, - "force": { - "type": "boolean", - "description": "Force pull" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error pulling changes", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/discard": { - "post": { - "summary": "Discard changes", - "description": "Discard changes to specified paths or all changes", - "operationId": "gitDiscard", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "paths": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Paths to discard changes for" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "paths": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Paths that were discarded" - } - } - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error discarding changes", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/commit": { - "post": { - "summary": "Commit changes", - "description": "Commit changes to the local repository", - "operationId": "gitCommit", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "paths": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Paths to commit" - }, - "message": { - "type": "string", - "description": "Commit message" - }, - "push": { - "type": "boolean", - "description": "Whether to push after committing" - } - }, - "required": ["message"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "shellId": { - "type": "string", - "description": "ID of the shell process" - } - }, - "required": ["shellId"] - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error committing changes", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/push": { - "post": { - "summary": "Push changes", - "description": "Push changes to the remote repository", - "operationId": "gitPush", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error pushing changes", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/pushToRemote": { - "post": { - "summary": "Push to remote", - "description": "Push changes to a specific remote repository and branch", - "operationId": "gitPushToRemote", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "URL of the remote repository" - }, - "branch": { - "type": "string", - "description": "Branch to push to" - }, - "squashAllCommits": { - "type": "boolean", - "description": "Whether to squash all commits before pushing" - } - }, - "required": ["url", "branch"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error pushing to remote", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/renameBranch": { - "post": { - "summary": "Rename branch", - "description": "Rename a branch in the local repository", - "operationId": "gitRenameBranch", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "oldBranch": { - "type": "string", - "description": "Name of the branch to rename" - }, - "newBranch": { - "type": "string", - "description": "New name for the branch" - } - }, - "required": ["oldBranch", "newBranch"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error renaming branch", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/remoteContent": { - "post": { - "summary": "Get remote content", - "description": "Retrieve the content of a file from a remote branch or commit", - "operationId": "gitRemoteContent", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GitRemoteParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "Content of the file" - } - }, - "required": ["content"] - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving remote content", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/diffStatus": { - "post": { - "summary": "Get diff status", - "description": "Retrieve the status of changes between two references", - "operationId": "gitDiffStatus", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GitDiffStatusParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/GitDiffStatusResult" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving diff status", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/resetLocalWithRemote": { - "post": { - "summary": "Reset local with remote", - "description": "Reset the local repository to match the remote", - "operationId": "gitResetLocalWithRemote", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error resetting local with remote", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/checkoutInitialBranch": { - "post": { - "summary": "Checkout initial branch", - "description": "Checkout the initial branch of the repository", - "operationId": "gitCheckoutInitialBranch", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error checking out initial branch", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/transposeLines": { - "post": { - "summary": "Transpose lines", - "description": "Map line numbers between different git commits", - "operationId": "gitTransposeLines", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "sha": { - "type": "string", - "description": "Commit SHA" - }, - "path": { - "type": "string", - "description": "File path" - }, - "line": { - "type": "number", - "description": "Line number" - } - }, - "required": ["sha", "path", "line"] - } - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "array", - "items": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "File path" - }, - "line": { - "type": "number", - "description": "Line number" - } - }, - "required": ["path", "line"], - "nullable": true - } - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error transposing lines", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "SuccessResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [0], - "description": "Status code for successful operations" - }, - "result": { - "type": "object", - "description": "Result payload for the operation" - } - }, - "required": ["status", "result"] - }, - "ErrorResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [1], - "description": "Status code for error operations" - }, - "error": { - "type": "object", - "description": "Error details" - } - }, - "required": ["status", "error"] - }, - "CommonError": { - "type": "object", - "properties": { - "code": { - "type": "number", - "description": "Error code" - }, - "message": { - "type": "string", - "description": "Error message" - }, - "data": { - "type": "object", - "description": "Additional error data", - "nullable": true - } - }, - "required": ["code", "message"] - }, - "GitStatusShortFormat": { - "type": "string", - "enum": ["", "M", "A", "D", "R", "C", "U", "?"], - "description": "Git status short format codes" - }, - "GitItem": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "File path" - }, - "index": { - "$ref": "#/components/schemas/GitStatusShortFormat" - }, - "workingTree": { - "$ref": "#/components/schemas/GitStatusShortFormat" - }, - "isStaged": { - "type": "boolean", - "description": "Whether the file is staged" - }, - "isConflicted": { - "type": "boolean", - "description": "Whether the file has conflicts" - }, - "fileId": { - "type": "string", - "description": "File ID" - } - }, - "required": ["path", "index", "workingTree", "isStaged", "isConflicted"] - }, - "GitChangedFiles": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/GitItem" - }, - "description": "Map of file IDs to GitItems" - }, - "GitBranchProperties": { - "type": "object", - "properties": { - "head": { - "type": "string", - "nullable": true, - "description": "Head commit" - }, - "branch": { - "type": "string", - "nullable": true, - "description": "Branch name" - }, - "ahead": { - "type": "number", - "description": "Number of commits ahead" - }, - "behind": { - "type": "number", - "description": "Number of commits behind" - }, - "safe": { - "type": "boolean", - "description": "Whether the branch is safe to use" - } - }, - "required": ["ahead", "behind", "safe"] - }, - "GitCommit": { - "type": "object", - "properties": { - "hash": { - "type": "string", - "description": "Commit hash" - }, - "date": { - "type": "string", - "description": "Commit date" - }, - "message": { - "type": "string", - "description": "Commit message" - }, - "author": { - "type": "string", - "description": "Commit author" - } - }, - "required": ["hash", "date", "message", "author"] - }, - "GitStatus": { - "type": "object", - "properties": { - "changedFiles": { - "$ref": "#/components/schemas/GitChangedFiles" - }, - "deletedFiles": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GitItem" - } - }, - "conflicts": { - "type": "boolean", - "description": "Whether there are remote conflicts" - }, - "localChanges": { - "type": "boolean", - "description": "Whether there are local changes" - }, - "remote": { - "$ref": "#/components/schemas/GitBranchProperties" - }, - "target": { - "$ref": "#/components/schemas/GitBranchProperties" - }, - "head": { - "type": "string", - "description": "Current HEAD commit" - }, - "commits": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GitCommit" - } - }, - "branch": { - "type": "string", - "nullable": true, - "description": "Current branch name" - }, - "isMerging": { - "type": "boolean", - "description": "Whether a merge is in progress" - } - }, - "required": [ - "changedFiles", - "deletedFiles", - "conflicts", - "localChanges", - "remote", - "target", - "commits", - "branch", - "isMerging" - ] - }, - "GitTargetDiff": { - "type": "object", - "properties": { - "ahead": { - "type": "number", - "description": "Number of commits ahead of target" - }, - "behind": { - "type": "number", - "description": "Number of commits behind target" - }, - "commits": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GitCommit" - } - } - }, - "required": ["ahead", "behind", "commits"] - }, - "GitRemotes": { - "type": "object", - "properties": { - "origin": { - "type": "string", - "description": "Origin remote URL" - }, - "upstream": { - "type": "string", - "description": "Upstream remote URL" - } - }, - "required": ["origin", "upstream"] - }, - "GitRemoteParams": { - "type": "object", - "properties": { - "reference": { - "type": "string", - "description": "Branch or commit hash" - }, - "path": { - "type": "string", - "description": "File path" - } - }, - "required": ["reference", "path"] - }, - "GitDiffStatusParams": { - "type": "object", - "properties": { - "base": { - "type": "string", - "description": "Base reference for diffing" - }, - "head": { - "type": "string", - "description": "Head reference for diffing" - } - }, - "required": ["base", "head"] - }, - "GitDiffStatusItem": { - "type": "object", - "properties": { - "status": { - "$ref": "#/components/schemas/GitStatusShortFormat" - }, - "path": { - "type": "string", - "description": "File path" - }, - "oldPath": { - "type": "string", - "description": "Original file path (for renames)" - }, - "hunks": { - "type": "array", - "items": { - "type": "object", - "properties": { - "original": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": ["start", "end"] - }, - "modified": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": ["start", "end"] - } - }, - "required": ["original", "modified"] - } - } - }, - "required": ["status", "path", "hunks"] - }, - "GitDiffStatusResult": { - "type": "object", - "properties": { - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GitDiffStatusItem" - } - } - }, - "required": ["files"] - } - } - } -} diff --git a/openapi-git.json b/openapi-git.json deleted file mode 100644 index e69de29..0000000 diff --git a/openapi-port.json b/openapi-port.json deleted file mode 100644 index 176f301..0000000 --- a/openapi-port.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Port API", - "description": "API for managing sandbox port operations", - "version": "1.0.0" - }, - "paths": { - "/port/list": { - "post": { - "summary": "List ports", - "description": "Retrieve a list of available ports and their URLs", - "operationId": "portList", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "list": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Port" - }, - "description": "List of available ports" - } - }, - "required": ["list"] - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error listing ports", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "SuccessResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [0], - "description": "Status code for successful operations" - }, - "result": { - "type": "object", - "description": "Result payload for the operation" - } - }, - "required": ["status", "result"] - }, - "ErrorResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [1], - "description": "Status code for error operations" - }, - "error": { - "type": "object", - "description": "Error details" - } - }, - "required": ["status", "error"] - }, - "CommonError": { - "type": "object", - "properties": { - "code": { - "type": "number", - "description": "Error code" - }, - "message": { - "type": "string", - "description": "Error message" - }, - "data": { - "type": "object", - "description": "Additional error data", - "nullable": true - } - }, - "required": ["code", "message"] - }, - "Port": { - "type": "object", - "properties": { - "port": { - "type": "number", - "description": "Port number" - }, - "url": { - "type": "string", - "description": "URL to access the service on this port" - } - }, - "required": ["port", "url"] - } - } - } -} diff --git a/openapi-sandbox-container.json b/openapi-sandbox-container.json deleted file mode 100644 index f272b37..0000000 --- a/openapi-sandbox-container.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Sandbox Container API", - "description": "API for managing sandbox container operations", - "version": "1.0.0" - }, - "paths": { - "/container/setup": { - "post": { - "summary": "Setup container", - "description": "Set up a new container based on a template", - "operationId": "containerSetup", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "templateId": { - "type": "string", - "description": "Identifier of the template to use" - }, - "templateArgs": { - "type": "object", - "description": "Arguments for the template", - "additionalProperties": { - "type": "string" - } - }, - "features": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Feature identifier" - }, - "options": { - "type": "object", - "description": "Options for the feature", - "additionalProperties": { - "type": "string" - } - } - }, - "required": ["id", "options"] - }, - "nullable": true - } - }, - "required": ["templateId", "templateArgs"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/TaskDTO" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error setting up container", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/ProtocolError" - } - } - } - ] - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "SuccessResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [0], - "description": "Status code for successful operations" - }, - "result": { - "type": "object", - "description": "Result payload for the operation" - } - }, - "required": ["status", "result"] - }, - "ErrorResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [1], - "description": "Status code for error operations" - }, - "error": { - "type": "object", - "description": "Error details" - } - }, - "required": ["status", "error"] - }, - "ProtocolError": { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "Error code" - }, - "message": { - "type": "string", - "description": "Error message" - }, - "data": { - "type": "object", - "description": "Additional error data", - "nullable": true - } - }, - "required": ["code", "message"] - }, - "TaskDTO": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Task identifier" - }, - "status": { - "type": "string", - "description": "Task status" - }, - "progress": { - "type": "number", - "description": "Task progress (0-100)" - } - }, - "required": ["id", "status", "progress"] - } - } - } -} diff --git a/openapi-sandbox-fs.json b/openapi-sandbox-fs.json deleted file mode 100644 index 57c3c6c..0000000 --- a/openapi-sandbox-fs.json +++ /dev/null @@ -1,2005 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Sandbox Rest FS API", - "description": "FS API for interacting with sandbox", - "version": "1.0.0" - }, - "paths": { - "/fs/writeFile": { - "post": { - "summary": "Write to a file", - "description": "Write content to a file at the specified path", - "operationId": "writeFile", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WriteFileRequest" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": {} - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error writing file", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "oneOf": [ - { - "$ref": "#/components/schemas/DefaultError" - }, - { - "$ref": "#/components/schemas/RawFsError" - } - ], - "discriminator": { - "propertyName": "code" - } - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/read": { - "post": { - "summary": "Read file system", - "description": "Retrieve the latest snapshot of the server's MemoryFS file and children list", - "operationId": "fsRead", - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/FSReadResult" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error reading file system", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/DefaultError" - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/operation": { - "post": { - "summary": "Perform file system operation", - "description": "Send a tree operation reflecting filesystem operations", - "operationId": "fsOperation", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FSOperationRequest" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/FSOperationResult" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error performing operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/DefaultError" - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/search": { - "post": { - "summary": "Search files", - "description": "Search for content in files", - "operationId": "fsSearch", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FSSearchParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SearchResult" - } - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error searching files", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/DefaultError" - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/streamingSearch": { - "post": { - "summary": "Start streaming search", - "description": "Start a streaming search for content in files", - "operationId": "fsStreamingSearch", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FSStreamingSearchParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "searchId": { - "type": "string", - "description": "ID of the search operation" - } - } - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error starting streaming search", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/DefaultError" - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/cancelStreamingSearch": { - "post": { - "summary": "Cancel streaming search", - "description": "Cancel an ongoing streaming search", - "operationId": "fsCancelStreamingSearch", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "searchId": { - "type": "string", - "description": "ID of the search to cancel" - } - }, - "required": ["searchId"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "searchId": { - "type": "string", - "description": "ID of the cancelled search" - } - } - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error cancelling search", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/DefaultError" - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/pathSearch": { - "post": { - "summary": "Search file paths", - "description": "Search for file paths matching a pattern", - "operationId": "fsPathSearch", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PathSearchParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/PathSearchResult" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error searching paths", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/DefaultError" - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/upload": { - "post": { - "summary": "Upload file", - "description": "Upload a file to the specified parent directory", - "operationId": "fsUpload", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "parentId": { - "type": "string", - "description": "ID of the parent directory" - }, - "filename": { - "type": "string", - "description": "Name of the file to create" - }, - "content": { - "type": "string", - "format": "binary", - "description": "File content as binary data" - } - }, - "required": ["parentId", "filename", "content"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "fileId": { - "type": "string", - "description": "ID of the created file" - } - } - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error uploading file", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "oneOf": [ - { - "$ref": "#/components/schemas/DefaultError" - }, - { - "$ref": "#/components/schemas/InvalidIdError" - } - ], - "discriminator": { - "propertyName": "code" - } - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/download": { - "post": { - "summary": "Download files", - "description": "Download files at a specified path as a zip", - "operationId": "fsDownload", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Path to download" - }, - "excludes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Glob patterns of files/folders to exclude from the download" - } - }, - "required": ["path"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "downloadUrl": { - "type": "string", - "description": "URL to download the files from" - } - } - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error creating download", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/DefaultError" - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/readFile": { - "post": { - "summary": "Read file content", - "description": "Read the content of a file at the specified path", - "operationId": "fsReadFile", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FSReadFileParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/FSReadFileResult" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error reading file", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "oneOf": [ - { - "$ref": "#/components/schemas/DefaultError" - }, - { - "$ref": "#/components/schemas/RawFsError" - } - ], - "discriminator": { - "propertyName": "code" - } - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/readdir": { - "post": { - "summary": "Read directory contents", - "description": "List the contents of a directory at the specified path", - "operationId": "fsReadDir", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FSReadDirParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/FSReadDirResult" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error reading directory", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "oneOf": [ - { - "$ref": "#/components/schemas/DefaultError" - }, - { - "$ref": "#/components/schemas/RawFsError" - } - ], - "discriminator": { - "propertyName": "code" - } - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/stat": { - "post": { - "summary": "Get file/directory stats", - "description": "Get stats for a file or directory at the specified path", - "operationId": "fsStat", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FSStatParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/FSStatResult" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error getting stats", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "oneOf": [ - { - "$ref": "#/components/schemas/DefaultError" - }, - { - "$ref": "#/components/schemas/RawFsError" - } - ], - "discriminator": { - "propertyName": "code" - } - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/copy": { - "post": { - "summary": "Copy file/directory", - "description": "Copy a file or directory from one location to another", - "operationId": "fsCopy", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FSCopyParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": {} - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error copying file/directory", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "oneOf": [ - { - "$ref": "#/components/schemas/DefaultError" - }, - { - "$ref": "#/components/schemas/RawFsError" - } - ], - "discriminator": { - "propertyName": "code" - } - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/rename": { - "post": { - "summary": "Rename file/directory", - "description": "Rename a file or directory (move from one location to another)", - "operationId": "fsRename", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FSRenameParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": {} - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error renaming file/directory", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "oneOf": [ - { - "$ref": "#/components/schemas/DefaultError" - }, - { - "$ref": "#/components/schemas/RawFsError" - } - ], - "discriminator": { - "propertyName": "code" - } - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/remove": { - "post": { - "summary": "Remove file/directory", - "description": "Delete a file or directory at the specified path", - "operationId": "fsRemove", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FSRemoveParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": {} - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error removing file/directory", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "oneOf": [ - { - "$ref": "#/components/schemas/DefaultError" - }, - { - "$ref": "#/components/schemas/RawFsError" - } - ], - "discriminator": { - "propertyName": "code" - } - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/mkdir": { - "post": { - "summary": "Create directory", - "description": "Create a new directory at the specified path", - "operationId": "fsMkdir", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FSMkdirParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": {} - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error creating directory", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "oneOf": [ - { - "$ref": "#/components/schemas/DefaultError" - }, - { - "$ref": "#/components/schemas/RawFsError" - } - ], - "discriminator": { - "propertyName": "code" - } - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/watch": { - "post": { - "summary": "Watch file/directory", - "description": "Watch a file or directory for changes", - "operationId": "fsWatch", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FSWatchParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/FSWatchResult" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error watching file/directory", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "oneOf": [ - { - "$ref": "#/components/schemas/DefaultError" - }, - { - "$ref": "#/components/schemas/RawFsError" - } - ], - "discriminator": { - "propertyName": "code" - } - } - } - } - ] - } - } - } - } - } - } - }, - "/fs/unwatch": { - "post": { - "summary": "Stop watching file/directory", - "description": "Stop watching a file or directory for changes", - "operationId": "fsUnwatch", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FSUnwatchParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": {} - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error unwatching file/directory", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "oneOf": [ - { - "$ref": "#/components/schemas/DefaultError" - }, - { - "$ref": "#/components/schemas/RawFsError" - } - ], - "discriminator": { - "propertyName": "code" - } - } - } - } - ] - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "SuccessResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [0], - "description": "Status code for successful operations" - }, - "result": { - "type": "object", - "description": "Result payload for the operation" - } - }, - "required": ["status", "result"] - }, - "ErrorResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [1], - "description": "Status code for error operations" - }, - "error": { - "oneOf": [ - { - "$ref": "#/components/schemas/DefaultError" - }, - { - "$ref": "#/components/schemas/RawFsError" - } - ], - "discriminator": { - "propertyName": "code" - } - } - }, - "required": ["status", "error"] - }, - "DefaultError": { - "type": "object", - "properties": { - "code": { - "$ref": "#/components/schemas/PitcherErrorCode", - "description": "Error code identifying the type of error" - }, - "data": { - "type": "object", - "description": "Additional error details", - "nullable": true - }, - "publicMessage": { - "type": "string", - "description": "Human-readable error message that can be displayed to users", - "nullable": true - } - }, - "required": ["code"] - }, - "RawFsError": { - "type": "object", - "properties": { - "code": { - "type": "number", - "enum": [102], - "description": "RAWFS_ERROR code" - }, - "data": { - "type": "object", - "properties": { - "errno": { - "type": ["number", "null"], - "description": "File system error number, or null if not available" - } - }, - "required": ["errno"] - }, - "publicMessage": { - "type": "string", - "description": "Human-readable error message that can be displayed to users", - "nullable": true - } - }, - "required": ["code", "data"] - }, - "PitcherErrorCode": { - "type": "integer", - "description": "Enumeration of error codes", - "enum": [ - 0, 1, 2, 3, 100, 101, 102, 200, 201, 204, 300, 400, 404, 410, 420, - 430, 440, 450, 460, 470, 500, 600, 601, 602, 704, 800, 801, 802, 803, - 814 - ], - "x-enum-descriptions": [ - "CRITICAL_ERROR", - "FEATURE_UNAVAILABLE", - "NO_ACCESS", - "RATE_LIMIT", - "INVALID_ID", - "INVALID_PATH", - "RAWFS_ERROR", - "SHELL_NOT_ACCESSIBLE", - "SHELL_CLOSED", - "SHELL_NOT_FOUND", - "MODEL_NOT_FOUND", - "GIT_OPERATION_IN_PROGRESS", - "GIT_REMOTE_FILE_NOT_FOUND", - "GIT_FETCH_FAIL", - "GIT_PULL_CONFLICT", - "GIT_RESET_LOCAL_REMOTE_ERROR", - "GIT_PUSH_FAIL", - "GIT_RESET_CHECKOUT_INITIAL_BRANCH_FAIL", - "GIT_PULL_FAIL", - "GIT_TRANSPOSE_LINES_FAIL", - "CHANNEL_NOT_FOUND", - "CONFIG_FILE_ALREADY_EXISTS", - "TASK_NOT_FOUND", - "COMMAND_ALREADY_CONFIGURED", - "COMMAND_NOT_FOUND", - "AI_NOT_AVAILABLE", - "PROMPT_TOO_BIG", - "FAILED_TO_RESPOND", - "AI_TOO_FREQUENT_REQUESTS", - "AI_CHAT_NOT_FOUND" - ] - }, - "WriteFileRequest": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "File path to write to" - }, - "content": { - "type": "string", - "format": "binary", - "description": "File content as binary data (Uint8Array)" - }, - "create": { - "type": "boolean", - "description": "Whether to create the file if it doesn't exist", - "default": false - }, - "overwrite": { - "type": "boolean", - "description": "Whether to overwrite the file if it exists", - "default": false - } - }, - "required": ["path", "content"] - }, - "FSReadResult": { - "type": "object", - "properties": { - "treeNodes": { - "type": "array", - "items": { - "type": "object", - "description": "JSON representation of a node in the file system" - } - }, - "clock": { - "type": "number", - "description": "Current clock value for the file system" - } - }, - "required": ["treeNodes", "clock"] - }, - "FSOperationRequest": { - "type": "object", - "properties": { - "operation": { - "$ref": "#/components/schemas/FSOperation" - } - }, - "required": ["operation"] - }, - "FSOperation": { - "oneOf": [ - { - "$ref": "#/components/schemas/FSCreateOperation" - }, - { - "$ref": "#/components/schemas/FSDeleteOperation" - }, - { - "$ref": "#/components/schemas/FSMoveOperation" - } - ], - "discriminator": { - "propertyName": "type" - } - }, - "FSCreateOperation": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["create"] - }, - "parentId": { - "type": "string", - "description": "ID of the parent directory" - }, - "newEntry": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID of the new entry" - }, - "type": { - "type": "string", - "enum": ["directory", "file"], - "description": "Type of the node" - }, - "name": { - "type": "string", - "description": "Name of the new entry" - } - }, - "required": ["id", "type", "name"] - } - }, - "required": ["type", "parentId", "newEntry"] - }, - "FSDeleteOperation": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["delete"] - }, - "id": { - "type": "string", - "description": "ID of the entry to delete" - } - }, - "required": ["type", "id"] - }, - "FSMoveOperation": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["move"] - }, - "id": { - "type": "string", - "description": "ID of the entry to move" - }, - "parentId": { - "type": "string", - "description": "ID of the new parent directory", - "nullable": true - }, - "name": { - "type": "string", - "description": "New name for the entry", - "nullable": true - } - }, - "required": ["type", "id"] - }, - "FSOperationResult": { - "oneOf": [ - { - "type": "object", - "properties": { - "code": { - "type": "number", - "enum": [0], - "description": "Success code" - }, - "clock": { - "type": "number", - "description": "Current clock value" - } - }, - "required": ["code", "clock"] - }, - { - "type": "object", - "properties": { - "code": { - "type": "number", - "enum": [1], - "description": "Ignored code" - } - }, - "required": ["code"] - } - ], - "discriminator": { - "propertyName": "code" - } - }, - "FSSearchParams": { - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Text to search for" - }, - "glob": { - "type": "string", - "description": "Glob pattern to filter files", - "nullable": true - }, - "isRegex": { - "type": "boolean", - "description": "Whether to treat the search text as a regular expression", - "nullable": true - }, - "caseSensitivity": { - "type": "string", - "enum": ["smart", "enabled", "disabled"], - "description": "Case sensitivity setting for the search", - "nullable": true - } - }, - "required": ["text"] - }, - "SearchResult": { - "type": "object", - "properties": { - "fileId": { - "type": "string", - "description": "ID of the file containing the match" - }, - "lines": { - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Text of the line containing the match" - } - }, - "required": ["text"] - }, - "lineNumber": { - "type": "integer", - "description": "Line number of the match" - }, - "absoluteOffset": { - "type": "integer", - "description": "Absolute offset of the match in the file" - }, - "submatches": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SearchSubMatch" - } - } - }, - "required": [ - "fileId", - "lines", - "lineNumber", - "absoluteOffset", - "submatches" - ] - }, - "SearchSubMatch": { - "type": "object", - "properties": { - "match": { - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Matched text" - } - }, - "required": ["text"] - }, - "start": { - "type": "integer", - "description": "Start position of the match" - }, - "end": { - "type": "integer", - "description": "End position of the match" - } - }, - "required": ["match", "start", "end"] - }, - "FSStreamingSearchParams": { - "type": "object", - "properties": { - "searchId": { - "type": "string", - "description": "ID for the search operation" - }, - "text": { - "type": "string", - "description": "Text to search for" - }, - "glob": { - "type": "string", - "description": "Glob pattern to filter files", - "nullable": true - }, - "isRegex": { - "type": "boolean", - "description": "Whether to treat the search text as a regular expression", - "nullable": true - }, - "caseSensitivity": { - "type": "string", - "enum": ["smart", "enabled", "disabled"], - "description": "Case sensitivity setting for the search", - "nullable": true - }, - "maxResults": { - "type": "integer", - "description": "Maximum number of results to return (default: 10,000)", - "nullable": true - } - }, - "required": ["searchId", "text"] - }, - "PathSearchParams": { - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Text to search for in file paths" - } - }, - "required": ["text"] - }, - "PathSearchResult": { - "type": "object", - "properties": { - "matches": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PathSearchMatch" - } - } - }, - "required": ["matches"] - }, - "PathSearchMatch": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Path that matched the search" - }, - "submatches": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SearchSubMatch" - } - } - }, - "required": ["path", "submatches"] - }, - "InvalidIdError": { - "type": "object", - "properties": { - "code": { - "type": "number", - "enum": [100], - "description": "INVALID_ID error code" - } - }, - "required": ["code"] - }, - "FSReadFileParams": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Path to the file to read" - } - }, - "required": ["path"] - }, - "FSReadFileResult": { - "type": "object", - "properties": { - "content": { - "type": "string", - "format": "binary", - "description": "File content as binary data" - } - }, - "required": ["content"] - }, - "FSReadDirParams": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Path to the directory to read" - } - }, - "required": ["path"] - }, - "FSReadDirResult": { - "type": "object", - "properties": { - "entries": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the entry" - }, - "type": { - "type": "string", - "enum": ["directory", "file"], - "description": "Type of the entry" - }, - "isSymlink": { - "type": "boolean", - "description": "Whether the entry is a symlink" - } - }, - "required": ["name", "type", "isSymlink"] - } - } - }, - "required": ["entries"] - }, - "FSStatParams": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Path to the file or directory to stat" - } - }, - "required": ["path"] - }, - "FSStatResult": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["directory", "file"], - "description": "Type of the entry" - }, - "isSymlink": { - "type": "boolean", - "description": "Whether the entry is a symlink" - }, - "size": { - "type": "integer", - "description": "Size of the file in bytes" - }, - "mtime": { - "type": "integer", - "description": "Last modified time" - }, - "ctime": { - "type": "integer", - "description": "Creation time" - }, - "atime": { - "type": "integer", - "description": "Last accessed time" - } - }, - "required": ["type", "isSymlink", "size", "mtime", "ctime", "atime"] - }, - "FSCopyParams": { - "type": "object", - "properties": { - "from": { - "type": "string", - "description": "Path to copy from" - }, - "to": { - "type": "string", - "description": "Path to copy to" - }, - "recursive": { - "type": "boolean", - "description": "Whether to copy directories recursively", - "nullable": true - }, - "overwrite": { - "type": "boolean", - "description": "Whether to overwrite existing files", - "nullable": true - } - }, - "required": ["from", "to"] - }, - "FSRenameParams": { - "type": "object", - "properties": { - "from": { - "type": "string", - "description": "Path to rename from" - }, - "to": { - "type": "string", - "description": "Path to rename to" - }, - "overwrite": { - "type": "boolean", - "description": "Whether to overwrite existing files", - "nullable": true - } - }, - "required": ["from", "to"] - }, - "FSRemoveParams": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Path to remove" - }, - "recursive": { - "type": "boolean", - "description": "Whether to remove directories recursively", - "nullable": true - } - }, - "required": ["path"] - }, - "FSMkdirParams": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Path to create directory at" - }, - "recursive": { - "type": "boolean", - "description": "Whether to create parent directories if they don't exist", - "nullable": true - } - }, - "required": ["path"] - }, - "FSWatchParams": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "Path to watch" - }, - "recursive": { - "type": "boolean", - "description": "Whether to watch directories recursively", - "nullable": true - }, - "excludes": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Glob patterns to exclude from watching", - "nullable": true - } - }, - "required": ["path"] - }, - "FSWatchResult": { - "type": "object", - "properties": { - "watchId": { - "type": "string", - "description": "ID of the watch" - } - }, - "required": ["watchId"] - }, - "FSUnwatchParams": { - "type": "object", - "properties": { - "watchId": { - "type": "string", - "description": "ID of the watch to stop" - } - }, - "required": ["watchId"] - } - } - } -} diff --git a/openapi-sandbox-git.json b/openapi-sandbox-git.json deleted file mode 100644 index 827a6e9..0000000 --- a/openapi-sandbox-git.json +++ /dev/null @@ -1,1369 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Sandbox Git API", - "description": "API for managing git operations in CodeSandbox", - "version": "1.0.0" - }, - "paths": { - "/git/status": { - "post": { - "summary": "Get git status", - "description": "Retrieve current git status including changed files, branch information, and commits", - "operationId": "gitStatus", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/GitStatus" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving git status", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/remotes": { - "post": { - "summary": "Get git remotes", - "description": "Retrieve git remote information", - "operationId": "gitRemotes", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/GitRemotes" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving git remotes", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/targetDiff": { - "post": { - "summary": "Get git target diff", - "description": "Retrieve diff between current branch and target branch", - "operationId": "gitTargetDiff", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "branch": { - "type": "string", - "description": "Branch to compare against" - } - }, - "required": ["branch"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/GitTargetDiff" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving git target diff", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/pull": { - "post": { - "summary": "Pull from remote", - "description": "Pull changes from remote repository", - "operationId": "gitPull", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "branch": { - "type": "string", - "description": "Branch to pull from" - }, - "force": { - "type": "boolean", - "description": "Force pull even if there are conflicts" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error pulling from remote", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/discard": { - "post": { - "summary": "Discard changes", - "description": "Discard local changes for specified paths", - "operationId": "gitDiscard", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "paths": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Paths of files to discard changes" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "paths": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error discarding changes", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/commit": { - "post": { - "summary": "Commit changes", - "description": "Commit changes to the repository", - "operationId": "gitCommit", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "paths": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Paths of files to commit" - }, - "message": { - "type": "string", - "description": "Commit message" - }, - "push": { - "type": "boolean", - "description": "Whether to push the commit immediately" - } - }, - "required": ["message"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "shellId": { - "type": "string", - "description": "ID of the shell process" - } - }, - "required": ["shellId"] - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error committing changes", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/push": { - "post": { - "summary": "Push changes", - "description": "Push local commits to remote repository", - "operationId": "gitPush", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error pushing changes", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/pushToRemote": { - "post": { - "summary": "Push to remote", - "description": "Push to a specific remote repository", - "operationId": "gitPushToRemote", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "description": "URL of the remote repository" - }, - "branch": { - "type": "string", - "description": "Branch to push to" - }, - "squashAllCommits": { - "type": "boolean", - "description": "Whether to squash all commits into one" - } - }, - "required": ["url", "branch"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error pushing to remote", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/renameBranch": { - "post": { - "summary": "Rename branch", - "description": "Rename a git branch", - "operationId": "gitRenameBranch", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "oldBranch": { - "type": "string", - "description": "Current branch name" - }, - "newBranch": { - "type": "string", - "description": "New branch name" - } - }, - "required": ["oldBranch", "newBranch"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error renaming branch", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/remoteContent": { - "post": { - "summary": "Get remote content", - "description": "Retrieve content from a remote repository", - "operationId": "gitRemoteContent", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GitRemoteParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "Content of the file" - } - }, - "required": ["content"] - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving remote content", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/diffStatus": { - "post": { - "summary": "Get diff status", - "description": "Retrieve diff status between two git references", - "operationId": "gitDiffStatus", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GitDiffStatusParams" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/GitDiffStatusResult" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving diff status", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/resetLocalWithRemote": { - "post": { - "summary": "Reset local with remote", - "description": "Reset local repository to match the remote state", - "operationId": "gitResetLocalWithRemote", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error resetting local with remote", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/checkoutInitialBranch": { - "post": { - "summary": "Checkout initial branch", - "description": "Checkout the initial branch of the repository", - "operationId": "gitCheckoutInitialBranch", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error checking out initial branch", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/git/transposeLines": { - "post": { - "summary": "Transpose lines", - "description": "Transpose line numbers from one git reference to another", - "operationId": "gitTransposeLines", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "sha": { - "type": "string", - "description": "Git commit SHA" - }, - "path": { - "type": "string", - "description": "Path to the file" - }, - "line": { - "type": "number", - "description": "Line number to transpose" - } - }, - "required": ["sha", "path", "line"] - } - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "array", - "items": { - "oneOf": [ - { - "type": "object", - "properties": { - "path": { - "type": "string" - }, - "line": { - "type": "number" - } - }, - "required": ["path", "line"] - }, - { - "type": "null" - } - ] - } - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error transposing lines", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "SuccessResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [0], - "description": "Status code for successful operations" - }, - "result": { - "type": "object", - "description": "Result payload for the operation" - } - }, - "required": ["status", "result"] - }, - "ErrorResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [1], - "description": "Status code for error operations" - }, - "error": { - "type": "object", - "description": "Error details" - } - }, - "required": ["status", "error"] - }, - "CommonError": { - "oneOf": [ - { - "type": "object", - "properties": { - "code": { - "type": "string", - "enum": [ - "GIT_OPERATION_IN_PROGRESS", - "GIT_REMOTE_FILE_NOT_FOUND" - ], - "description": "Error code" - }, - "message": { - "type": "string", - "description": "Error message" - } - }, - "required": ["code", "message"] - }, - { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "Protocol error code" - }, - "message": { - "type": "string", - "description": "Error message" - }, - "data": { - "type": "object", - "description": "Additional error data" - } - }, - "required": ["code", "message"] - } - ] - }, - "GitStatusShortFormat": { - "type": "string", - "enum": ["", "M", "A", "D", "R", "C", "U", "?"], - "description": "Git status short format codes" - }, - "GitItem": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "File path" - }, - "index": { - "$ref": "#/components/schemas/GitStatusShortFormat" - }, - "workingTree": { - "$ref": "#/components/schemas/GitStatusShortFormat" - }, - "isStaged": { - "type": "boolean", - "description": "Whether the file is staged" - }, - "isConflicted": { - "type": "boolean", - "description": "Whether the file has conflicts" - }, - "fileId": { - "type": "string", - "description": "Unique identifier for the file" - } - }, - "required": ["path", "index", "workingTree", "isStaged", "isConflicted"] - }, - "GitChangedFiles": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/GitItem" - }, - "description": "Map of file IDs to Git items" - }, - "GitBranchProperties": { - "type": "object", - "properties": { - "head": { - "type": ["string", "null"], - "description": "Current HEAD reference" - }, - "branch": { - "type": ["string", "null"], - "description": "Current branch name" - }, - "ahead": { - "type": "number", - "description": "Number of commits ahead of the remote" - }, - "behind": { - "type": "number", - "description": "Number of commits behind the remote" - }, - "safe": { - "type": "boolean", - "description": "Whether the branch is safe to operate on" - } - }, - "required": ["ahead", "behind", "safe"] - }, - "GitCommit": { - "type": "object", - "properties": { - "hash": { - "type": "string", - "description": "Commit hash" - }, - "date": { - "type": "string", - "description": "Commit date" - }, - "message": { - "type": "string", - "description": "Commit message" - }, - "author": { - "type": "string", - "description": "Commit author" - } - }, - "required": ["hash", "date", "message", "author"] - }, - "GitStatus": { - "type": "object", - "properties": { - "changedFiles": { - "$ref": "#/components/schemas/GitChangedFiles" - }, - "deletedFiles": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GitItem" - } - }, - "conflicts": { - "type": "boolean", - "description": "Whether there are remote conflicts" - }, - "localChanges": { - "type": "boolean", - "description": "Whether there are local changes" - }, - "remote": { - "$ref": "#/components/schemas/GitBranchProperties" - }, - "target": { - "$ref": "#/components/schemas/GitBranchProperties" - }, - "head": { - "type": "string", - "description": "Current HEAD reference" - }, - "commits": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GitCommit" - } - }, - "branch": { - "type": ["string", "null"], - "description": "Current branch name" - }, - "isMerging": { - "type": "boolean", - "description": "Whether a merge is in progress" - } - }, - "required": [ - "changedFiles", - "deletedFiles", - "conflicts", - "localChanges", - "remote", - "target", - "commits", - "branch", - "isMerging" - ] - }, - "GitTargetDiff": { - "type": "object", - "properties": { - "ahead": { - "type": "number", - "description": "Number of commits ahead of the target" - }, - "behind": { - "type": "number", - "description": "Number of commits behind the target" - }, - "commits": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GitCommit" - } - } - }, - "required": ["ahead", "behind", "commits"] - }, - "GitRemotes": { - "type": "object", - "properties": { - "origin": { - "type": "string", - "description": "Origin remote URL" - }, - "upstream": { - "type": "string", - "description": "Upstream remote URL" - } - }, - "required": ["origin", "upstream"] - }, - "GitRemoteParams": { - "type": "object", - "properties": { - "reference": { - "type": "string", - "description": "Branch or commit hash" - }, - "path": { - "type": "string", - "description": "Path to the file" - } - }, - "required": ["reference", "path"] - }, - "GitDiffStatusParams": { - "type": "object", - "properties": { - "base": { - "type": "string", - "description": "Base reference used for diffing" - }, - "head": { - "type": "string", - "description": "Head reference used for diffing" - } - }, - "required": ["base", "head"] - }, - "GitDiffStatusItem": { - "type": "object", - "properties": { - "status": { - "$ref": "#/components/schemas/GitStatusShortFormat" - }, - "path": { - "type": "string", - "description": "Path to the file" - }, - "oldPath": { - "type": "string", - "description": "Original path for renamed files" - }, - "hunks": { - "type": "array", - "items": { - "type": "object", - "properties": { - "original": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": ["start", "end"] - }, - "modified": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": ["start", "end"] - } - }, - "required": ["original", "modified"] - } - } - }, - "required": ["status", "path", "hunks"] - }, - "GitDiffStatusResult": { - "type": "object", - "properties": { - "files": { - "type": "array", - "items": { - "$ref": "#/components/schemas/GitDiffStatusItem" - } - } - }, - "required": ["files"] - } - } - } -} diff --git a/openapi-sandbox-setup.json b/openapi-sandbox-setup.json deleted file mode 100644 index bc8b5c4..0000000 --- a/openapi-sandbox-setup.json +++ /dev/null @@ -1,570 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Sandbox Setup API", - "description": "API for managing sandbox setup operations", - "version": "1.0.0" - }, - "paths": { - "/setup/get": { - "post": { - "summary": "Get setup progress", - "description": "Retrieve the current setup progress status", - "operationId": "setupGet", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/SetupProgress" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving setup progress", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/ProtocolError" - } - } - } - ] - } - } - } - } - } - } - }, - "/setup/skip": { - "post": { - "summary": "Skip setup step", - "description": "Skip a specific step in the setup process", - "operationId": "setupSkipStep", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "stepIndexToSkip": { - "type": "number", - "description": "Index of the step to skip" - } - }, - "required": ["stepIndexToSkip"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/SetupProgress" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error skipping step", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/ProtocolError" - } - } - } - ] - } - } - } - } - } - } - }, - "/setup/skipAll": { - "post": { - "summary": "Skip all setup steps", - "description": "Skip all remaining steps in the setup process", - "operationId": "setupSkipAll", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "null" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/SetupProgress" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error skipping all steps", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/ProtocolError" - } - } - } - ] - } - } - } - } - } - } - }, - "/setup/disable": { - "post": { - "summary": "Disable setup", - "description": "Disable the setup process", - "operationId": "setupDisable", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "null" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/SetupProgress" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error disabling setup", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/ProtocolError" - } - } - } - ] - } - } - } - } - } - } - }, - "/setup/enable": { - "post": { - "summary": "Enable setup", - "description": "Enable the setup process", - "operationId": "setupEnable", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "null" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/SetupProgress" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error enabling setup", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/ProtocolError" - } - } - } - ] - } - } - } - } - } - } - }, - "/setup/init": { - "post": { - "summary": "Initialize setup", - "description": "Initialize the setup process", - "operationId": "setupInit", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "null" - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/SetupProgress" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error initializing setup", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/ProtocolError" - } - } - } - ] - } - } - } - } - } - } - }, - "/setup/setStep": { - "post": { - "summary": "Set current setup step", - "description": "Set the current step in the setup process (used for restarting)", - "operationId": "setupSetStep", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "stepIndex": { - "type": "number", - "description": "Index of the step to set as current" - } - }, - "required": ["stepIndex"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/SetupProgress" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error setting current step", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/ProtocolError" - } - } - } - ] - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "SuccessResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [0], - "description": "Status code for successful operations" - }, - "result": { - "type": "object", - "description": "Result payload for the operation" - } - }, - "required": ["status", "result"] - }, - "ErrorResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [1], - "description": "Status code for error operations" - }, - "error": { - "type": "object", - "description": "Error details" - } - }, - "required": ["status", "error"] - }, - "ProtocolError": { - "type": "object", - "properties": { - "code": { - "type": "number", - "description": "Error code" - }, - "message": { - "type": "string", - "description": "Error message" - }, - "data": { - "type": "object", - "description": "Additional error data", - "nullable": true - } - }, - "required": ["code", "message"] - }, - "SetupShellStatus": { - "type": "string", - "enum": ["SUCCEEDED", "FAILED", "SKIPPED"], - "description": "Status of a setup shell step" - }, - "Step": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the setup step" - }, - "command": { - "type": "string", - "description": "Command to execute for this step" - }, - "shellId": { - "type": "string", - "description": "ID of the shell executing the command", - "nullable": true - }, - "finishStatus": { - "$ref": "#/components/schemas/SetupShellStatus", - "nullable": true, - "description": "Status of the step after completion" - } - }, - "required": ["name", "command", "shellId", "finishStatus"] - }, - "SetupProgress": { - "type": "object", - "properties": { - "state": { - "type": "string", - "enum": ["IDLE", "IN_PROGRESS", "FINISHED", "STOPPED"], - "description": "Current state of the setup process" - }, - "steps": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Step" - }, - "description": "List of setup steps" - }, - "currentStepIndex": { - "type": "number", - "description": "Index of the current step being executed" - } - }, - "required": ["state", "steps", "currentStepIndex"] - } - } - } -} diff --git a/openapi-sandbox-shell.json b/openapi-sandbox-shell.json deleted file mode 100644 index e362489..0000000 --- a/openapi-sandbox-shell.json +++ /dev/null @@ -1,916 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Sandbox Shell API", - "description": "API for managing terminal and command shells in the sandbox", - "version": "1.0.0" - }, - "paths": { - "/shell/create": { - "post": { - "summary": "Create a new shell", - "description": "Creates a new terminal or command shell", - "operationId": "shellCreate", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "Command to execute in the shell" - }, - "cwd": { - "type": "string", - "description": "Working directory for the shell" - }, - "size": { - "$ref": "#/components/schemas/ShellSize", - "description": "Terminal size dimensions" - }, - "type": { - "$ref": "#/components/schemas/ShellProcessType", - "description": "Type of shell to create" - }, - "isSystemShell": { - "type": "boolean", - "description": "Whether this shell is started by the editor itself to run a specific process" - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/OpenShellDTO" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error creating shell", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/shell/in": { - "post": { - "summary": "Send input to shell", - "description": "Sends user input to an active shell", - "operationId": "shellIn", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "shellId": { - "$ref": "#/components/schemas/ShellId", - "description": "ID of the target shell" - }, - "input": { - "type": "string", - "description": "Input to send to the shell" - }, - "size": { - "$ref": "#/components/schemas/ShellSize", - "description": "Current terminal dimensions" - } - }, - "required": ["shellId", "input", "size"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error sending input to shell", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/shell/list": { - "post": { - "summary": "List all shells", - "description": "Retrieves a list of all available shells", - "operationId": "shellList", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": { - "shells": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ShellDTO" - } - } - }, - "required": ["shells"] - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error listing shells", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/shell/open": { - "post": { - "summary": "Open an existing shell", - "description": "Opens an existing shell and retrieves its buffer", - "operationId": "shellOpen", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "shellId": { - "$ref": "#/components/schemas/ShellId", - "description": "ID of the shell to open" - }, - "size": { - "$ref": "#/components/schemas/ShellSize", - "description": "Terminal dimensions" - } - }, - "required": ["shellId", "size"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/OpenShellDTO" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error opening shell", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/shell/close": { - "post": { - "summary": "Close a shell", - "description": "Closes a shell without terminating the underlying process", - "operationId": "shellClose", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "shellId": { - "$ref": "#/components/schemas/ShellId", - "description": "ID of the shell to close" - } - }, - "required": ["shellId"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error closing shell", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/shell/restart": { - "post": { - "summary": "Restart a shell", - "description": "Restarts an existing shell process", - "operationId": "shellRestart", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "shellId": { - "$ref": "#/components/schemas/ShellId", - "description": "ID of the shell to restart" - } - }, - "required": ["shellId"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error restarting shell", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/shell/terminate": { - "post": { - "summary": "Terminate a shell", - "description": "Terminates a shell and its underlying process", - "operationId": "shellTerminate", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "shellId": { - "$ref": "#/components/schemas/ShellId", - "description": "ID of the shell to terminate" - } - }, - "required": ["shellId"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/ShellDTO" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error terminating shell", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/shell/resize": { - "post": { - "summary": "Resize a shell", - "description": "Updates the dimensions of a shell", - "operationId": "shellResize", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "shellId": { - "$ref": "#/components/schemas/ShellId", - "description": "ID of the shell to resize" - }, - "size": { - "$ref": "#/components/schemas/ShellSize", - "description": "New terminal dimensions" - } - }, - "required": ["shellId", "size"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error resizing shell", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/shell/rename": { - "post": { - "summary": "Rename a shell", - "description": "Updates the name of a shell", - "operationId": "shellRename", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "shellId": { - "$ref": "#/components/schemas/ShellId", - "description": "ID of the shell to rename" - }, - "name": { - "type": "string", - "description": "New name for the shell" - } - }, - "required": ["shellId", "name"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error renaming shell", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "SuccessResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [0], - "description": "Status code for successful operations" - }, - "result": { - "type": "object", - "description": "Result payload for the operation" - } - }, - "required": ["status", "result"] - }, - "ErrorResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [1], - "description": "Status code for error operations" - }, - "error": { - "type": "object", - "description": "Error details" - } - }, - "required": ["status", "error"] - }, - "ShellId": { - "type": "string", - "description": "Unique identifier for a shell" - }, - "ShellSize": { - "type": "object", - "properties": { - "cols": { - "type": "number", - "description": "Number of columns in the terminal" - }, - "rows": { - "type": "number", - "description": "Number of rows in the terminal" - } - }, - "required": ["cols", "rows"] - }, - "ShellProcessType": { - "type": "string", - "enum": ["TERMINAL", "COMMAND"], - "description": "Type of shell process" - }, - "ShellProcessStatus": { - "type": "string", - "enum": ["RUNNING", "FINISHED", "ERROR", "KILLED", "RESTARTING"], - "description": "Current status of the shell process" - }, - "BaseShellDTO": { - "type": "object", - "properties": { - "shellId": { - "$ref": "#/components/schemas/ShellId" - }, - "name": { - "type": "string", - "description": "Display name of the shell" - }, - "status": { - "$ref": "#/components/schemas/ShellProcessStatus" - }, - "exitCode": { - "type": "number", - "description": "Exit code of the process if it has finished", - "nullable": true - } - }, - "required": ["shellId", "name", "status"] - }, - "CommandShellDTO": { - "allOf": [ - { - "$ref": "#/components/schemas/BaseShellDTO" - }, - { - "type": "object", - "properties": { - "shellType": { - "type": "string", - "enum": ["COMMAND"], - "description": "Indicates this is a command shell" - }, - "startCommand": { - "type": "string", - "description": "The command that was executed to start this shell" - } - }, - "required": ["shellType", "startCommand"] - } - ] - }, - "TerminalShellDTO": { - "allOf": [ - { - "$ref": "#/components/schemas/BaseShellDTO" - }, - { - "type": "object", - "properties": { - "shellType": { - "type": "string", - "enum": ["TERMINAL"], - "description": "Indicates this is a terminal shell" - }, - "ownerUsername": { - "type": "string", - "description": "Username of the shell owner" - }, - "isSystemShell": { - "type": "boolean", - "description": "Whether this is a system shell" - } - }, - "required": ["shellType", "ownerUsername", "isSystemShell"] - } - ] - }, - "ShellDTO": { - "oneOf": [ - { - "$ref": "#/components/schemas/CommandShellDTO" - }, - { - "$ref": "#/components/schemas/TerminalShellDTO" - } - ], - "discriminator": { - "propertyName": "shellType", - "mapping": { - "COMMAND": "#/components/schemas/CommandShellDTO", - "TERMINAL": "#/components/schemas/TerminalShellDTO" - } - } - }, - "OpenCommandShellDTO": { - "allOf": [ - { - "$ref": "#/components/schemas/CommandShellDTO" - }, - { - "type": "object", - "properties": { - "buffer": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Content buffer of the shell" - } - }, - "required": ["buffer"] - } - ] - }, - "OpenTerminalShellDTO": { - "allOf": [ - { - "$ref": "#/components/schemas/TerminalShellDTO" - }, - { - "type": "object", - "properties": { - "buffer": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Content buffer of the shell" - } - }, - "required": ["buffer"] - } - ] - }, - "OpenShellDTO": { - "oneOf": [ - { - "$ref": "#/components/schemas/OpenCommandShellDTO" - }, - { - "$ref": "#/components/schemas/OpenTerminalShellDTO" - } - ], - "discriminator": { - "propertyName": "shellType", - "mapping": { - "COMMAND": "#/components/schemas/OpenCommandShellDTO", - "TERMINAL": "#/components/schemas/OpenTerminalShellDTO" - } - } - }, - "CommonError": { - "oneOf": [ - { - "type": "object", - "properties": { - "code": { - "type": "string", - "enum": ["SHELL_NOT_ACCESSIBLE"], - "description": "Error code indicating the shell is not accessible" - }, - "message": { - "type": "string", - "description": "Error message" - } - }, - "required": ["code", "message"] - }, - { - "type": "object", - "properties": { - "code": { - "type": "string", - "description": "Protocol error code" - }, - "message": { - "type": "string", - "description": "Error message" - } - }, - "required": ["code", "message"] - } - ] - } - } - } -} diff --git a/openapi-sandbox-system.json b/openapi-sandbox-system.json deleted file mode 100644 index 501f2f5..0000000 --- a/openapi-sandbox-system.json +++ /dev/null @@ -1,348 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Sandbox System API", - "description": "API for managing sandbox system operations", - "version": "1.0.0" - }, - "paths": { - "/system/update": { - "post": { - "summary": "Update system", - "description": "Update the sandbox system", - "operationId": "systemUpdate", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "object", - "properties": {} - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error updating system", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/SystemError" - } - } - } - ] - } - } - } - } - } - } - }, - "/system/hibernate": { - "post": { - "summary": "Hibernate system", - "description": "Put the sandbox system into hibernation mode", - "operationId": "systemHibernate", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error hibernating system", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/SystemError" - } - } - } - ] - } - } - } - } - } - } - }, - "/system/metrics": { - "post": { - "summary": "Get system metrics", - "description": "Retrieve current system metrics including CPU, memory and storage usage", - "operationId": "systemMetrics", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/SystemMetricsStatus" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving system metrics", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/SystemError" - } - } - } - ] - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "SuccessResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [0], - "description": "Status code for successful operations" - }, - "result": { - "type": "object", - "description": "Result payload for the operation" - } - }, - "required": ["status", "result"] - }, - "ErrorResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [1], - "description": "Status code for error operations" - }, - "error": { - "type": "object", - "description": "Error details" - } - }, - "required": ["status", "error"] - }, - "SystemError": { - "type": "object", - "properties": { - "code": { - "type": "number", - "description": "Error code" - }, - "message": { - "type": "string", - "description": "Error message" - }, - "data": { - "type": "object", - "description": "Additional error data", - "nullable": true - } - }, - "required": ["code", "message"] - }, - "SystemMetricsStatus": { - "type": "object", - "properties": { - "cpu": { - "type": "object", - "properties": { - "cores": { - "type": "number", - "description": "Number of CPU cores" - }, - "used": { - "type": "number", - "description": "Used CPU resources" - }, - "configured": { - "type": "number", - "description": "Configured CPU resources" - } - }, - "required": ["cores", "used", "configured"] - }, - "memory": { - "type": "object", - "properties": { - "used": { - "type": "number", - "description": "Used memory in bytes" - }, - "total": { - "type": "number", - "description": "Total available memory in bytes" - }, - "configured": { - "type": "number", - "description": "Configured memory limit in bytes" - } - }, - "required": ["used", "total", "configured"] - }, - "storage": { - "type": "object", - "properties": { - "used": { - "type": "number", - "description": "Used storage in bytes" - }, - "total": { - "type": "number", - "description": "Total available storage in bytes" - }, - "configured": { - "type": "number", - "description": "Configured storage limit in bytes" - } - }, - "required": ["used", "total", "configured"] - } - }, - "required": ["cpu", "memory", "storage"] - }, - "InitStatus": { - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "Status message" - }, - "isError": { - "type": "boolean", - "description": "Whether the status represents an error", - "nullable": true - }, - "progress": { - "type": "number", - "description": "Current progress (0-100)", - "minimum": 0, - "maximum": 100 - }, - "nextProgress": { - "type": "number", - "description": "Next progress target (0-100)", - "minimum": 0, - "maximum": 100 - }, - "stdout": { - "type": "string", - "description": "Standard output from the initialization process", - "nullable": true - } - }, - "required": ["message", "progress", "nextProgress"] - } - } - } -} diff --git a/openapi-sandbox-task.json b/openapi-sandbox-task.json deleted file mode 100644 index 6b5e320..0000000 --- a/openapi-sandbox-task.json +++ /dev/null @@ -1,947 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "title": "Sandbox Task API", - "description": "API for managing tasks in sandbox", - "version": "1.0.0" - }, - "paths": { - "/task/list": { - "post": { - "summary": "List tasks", - "description": "Retrieve a list of all configured tasks", - "operationId": "taskList", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/TaskListDTO" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error retrieving task list", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/CommonError" - } - } - } - ] - } - } - } - } - } - } - }, - "/task/run": { - "post": { - "summary": "Run task", - "description": "Start execution of a task by ID", - "operationId": "taskRun", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "taskId": { - "type": "string", - "description": "ID of the task to run" - } - }, - "required": ["taskId"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/TaskDTO" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error running task", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/TaskError" - } - } - } - ] - } - } - } - } - } - } - }, - "/task/runCommand": { - "post": { - "summary": "Run command", - "description": "Run a shell command directly, optionally saving it as a task", - "operationId": "taskRunCommand", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "Command to run" - }, - "name": { - "type": "string", - "description": "Optional name for the task", - "nullable": true - }, - "saveToConfig": { - "type": "boolean", - "description": "Whether to save this command as a task in the config", - "nullable": true - } - }, - "required": ["command"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/TaskDTO" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error running command", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/TaskError" - } - } - } - ] - } - } - } - } - } - } - }, - "/task/stop": { - "post": { - "summary": "Stop task", - "description": "Stop execution of a running task", - "operationId": "taskStop", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "taskId": { - "type": "string", - "description": "ID of the task to stop" - } - }, - "required": ["taskId"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "oneOf": [ - { - "$ref": "#/components/schemas/TaskDTO" - }, - { - "type": "null", - "description": "Null when stopping an unconfigured task" - } - ] - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error stopping task", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/TaskError" - } - } - } - ] - } - } - } - } - } - } - }, - "/task/create": { - "post": { - "summary": "Create task", - "description": "Create a new task configuration", - "operationId": "taskCreate", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "taskFields": { - "$ref": "#/components/schemas/TaskDefinitionDTO" - }, - "startTask": { - "type": "boolean", - "description": "Whether to start the task immediately after creation", - "nullable": true - } - }, - "required": ["taskFields"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/TaskListDTO" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error creating task", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/TaskError" - } - } - } - ] - } - } - } - } - } - } - }, - "/task/update": { - "post": { - "summary": "Update task", - "description": "Update an existing task configuration", - "operationId": "taskUpdate", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "taskId": { - "type": "string", - "description": "ID of the task to update" - }, - "taskFields": { - "type": "object", - "description": "Fields to update in the task", - "properties": { - "name": { - "type": "string", - "description": "Name of the task", - "nullable": true - }, - "command": { - "type": "string", - "description": "Command to run", - "nullable": true - }, - "runAtStart": { - "type": "boolean", - "description": "Whether to run the task at sandbox start", - "nullable": true - }, - "preview": { - "type": "object", - "properties": { - "port": { - "type": "number", - "description": "Port to use for previewing the task", - "nullable": true - }, - "pr-link": { - "type": "string", - "enum": ["direct", "redirect", "devtool"], - "description": "Type of PR link to use", - "nullable": true - } - }, - "nullable": true - } - } - } - }, - "required": ["taskId", "taskFields"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/TaskDTO" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error updating task", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/TaskError" - } - } - } - ] - } - } - } - } - } - } - }, - "/task/saveToConfig": { - "post": { - "summary": "Save task to config", - "description": "Save a runtime task to the configuration file", - "operationId": "taskSaveToConfig", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "taskId": { - "type": "string", - "description": "ID of the task to save to config" - } - }, - "required": ["taskId"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "$ref": "#/components/schemas/TaskDTO" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error saving task to config", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/TaskError" - } - } - } - ] - } - } - } - } - } - } - }, - "/task/generateConfig": { - "post": { - "summary": "Generate task config", - "description": "Generate a configuration file from current tasks", - "operationId": "taskGenerateConfig", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": {} - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error generating config", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/TaskError" - } - } - } - ] - } - } - } - } - } - } - }, - "/task/createSetupTasks": { - "post": { - "summary": "Create setup tasks", - "description": "Create tasks that run during sandbox setup", - "operationId": "taskCreateSetupTasks", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tasks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TaskDefinitionDTO" - }, - "description": "Setup tasks to create" - } - }, - "required": ["tasks"] - } - } - } - }, - "responses": { - "200": { - "description": "Successful operation", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/SuccessResponse" - }, - { - "type": "object", - "properties": { - "result": { - "type": "null" - } - } - } - ] - } - } - } - }, - "400": { - "description": "Error creating setup tasks", - "content": { - "application/json": { - "schema": { - "allOf": [ - { - "$ref": "#/components/schemas/ErrorResponse" - }, - { - "type": "object", - "properties": { - "error": { - "$ref": "#/components/schemas/TaskError" - } - } - } - ] - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "SuccessResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [0], - "description": "Status code for successful operations" - }, - "result": { - "type": "object", - "description": "Result payload for the operation" - } - }, - "required": ["status", "result"] - }, - "ErrorResponse": { - "type": "object", - "properties": { - "status": { - "type": "number", - "enum": [1], - "description": "Status code for error operations" - }, - "error": { - "type": "object", - "description": "Error details" - } - }, - "required": ["status", "error"] - }, - "CommonError": { - "type": "object", - "properties": { - "code": { - "type": "number", - "description": "Error code" - }, - "message": { - "type": "string", - "description": "Error message" - }, - "data": { - "type": "object", - "description": "Additional error data", - "nullable": true - } - }, - "required": ["code"] - }, - "TaskError": { - "oneOf": [ - { - "type": "object", - "properties": { - "code": { - "type": "number", - "enum": [600], - "description": "CONFIG_FILE_ALREADY_EXISTS error code" - }, - "message": { - "type": "string", - "description": "Error message" - } - }, - "required": ["code", "message"] - }, - { - "type": "object", - "properties": { - "code": { - "type": "number", - "enum": [601], - "description": "TASK_NOT_FOUND error code" - }, - "message": { - "type": "string", - "description": "Error message" - } - }, - "required": ["code", "message"] - }, - { - "type": "object", - "properties": { - "code": { - "type": "number", - "enum": [602], - "description": "COMMAND_ALREADY_CONFIGURED error code" - }, - "message": { - "type": "string", - "description": "Error message" - } - }, - "required": ["code", "message"] - }, - { - "$ref": "#/components/schemas/CommonError" - } - ], - "discriminator": { - "propertyName": "code" - } - }, - "TaskDefinitionDTO": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name of the task" - }, - "command": { - "type": "string", - "description": "Command to run for the task" - }, - "runAtStart": { - "type": "boolean", - "description": "Whether the task should run when the sandbox starts", - "nullable": true - }, - "preview": { - "type": "object", - "properties": { - "port": { - "type": "number", - "description": "Port to preview from this task", - "nullable": true - }, - "pr-link": { - "type": "string", - "enum": ["direct", "redirect", "devtool"], - "description": "Type of PR link to use", - "nullable": true - } - }, - "nullable": true - } - }, - "required": ["name", "command"] - }, - "CommandShellDTO": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "ID of the shell command" - }, - "command": { - "type": "string", - "description": "Command being executed" - }, - "status": { - "type": "string", - "enum": ["initializing", "running", "stopped", "error"], - "description": "Current status of the shell command" - }, - "output": { - "type": "string", - "description": "Current output of the command" - } - }, - "required": ["id", "command", "status", "output"] - }, - "Port": { - "type": "object", - "properties": { - "port": { - "type": "number", - "description": "Port number" - }, - "hostname": { - "type": "string", - "description": "Hostname the port is bound to" - }, - "status": { - "type": "string", - "enum": ["open", "closed"], - "description": "Current status of the port" - }, - "taskId": { - "type": "string", - "description": "ID of the task that opened this port", - "nullable": true - } - }, - "required": ["port", "hostname", "status"] - }, - "TaskDTO": { - "allOf": [ - { - "$ref": "#/components/schemas/TaskDefinitionDTO" - }, - { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Unique ID of the task" - }, - "unconfigured": { - "type": "boolean", - "description": "Whether this task is unconfigured (not saved in config)", - "nullable": true - }, - "shell": { - "type": "object", - "nullable": true, - "allOf": [ - { - "$ref": "#/components/schemas/CommandShellDTO" - } - ] - }, - "ports": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Port" - }, - "description": "Ports opened by this task" - } - }, - "required": ["id", "shell", "ports"] - } - ] - }, - "TaskListDTO": { - "type": "object", - "properties": { - "tasks": { - "type": "object", - "additionalProperties": { - "$ref": "#/components/schemas/TaskDTO" - }, - "description": "Map of task IDs to task objects" - }, - "setupTasks": { - "type": "array", - "items": { - "$ref": "#/components/schemas/TaskDefinitionDTO" - }, - "description": "Tasks that run during sandbox setup" - }, - "validationErrors": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Validation errors in the task configuration" - } - }, - "required": ["tasks", "setupTasks", "validationErrors"] - } - } - } -} diff --git a/openapi.json b/openapi.json index 17bd4c6..caee078 100644 --- a/openapi.json +++ b/openapi.json @@ -941,6 +941,7 @@ "reconnect_token": { "type": "string" }, "use_pint": { "type": "boolean" }, "user_workspace_path": { "type": "string" }, + "vm_agent_type": { "type": "string" }, "workspace_path": { "type": "string" } }, "required": [ @@ -955,7 +956,8 @@ "reconnect_token", "use_pint", "user_workspace_path", - "workspace_path" + "workspace_path", + "vm_agent_type" ], "type": "object" } @@ -1555,6 +1557,7 @@ "reconnect_token": { "type": "string" }, "use_pint": { "type": "boolean" }, "user_workspace_path": { "type": "string" }, + "vm_agent_type": { "type": "string" }, "workspace_path": { "type": "string" } }, "required": [ @@ -1569,7 +1572,8 @@ "reconnect_token", "use_pint", "user_workspace_path", - "workspace_path" + "workspace_path", + "vm_agent_type" ], "type": "object" }, diff --git a/package.json b/package.json index 5cd880a..7ee1a76 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "build:cjs:types": "tsc -p ./tsconfig.build-cjs.json --emitDeclarationOnly", "build:esm:types": "tsc -p ./tsconfig.build-esm.json --emitDeclarationOnly", "build-openapi": "rimraf src/api-clients && curl -o openapi.json https://api.codesandbox.io/meta/openapi && npx prettier --write ./openapi.json && node_modules/.bin/openapi-ts -i ./openapi.json -o src/api-clients/client -c @hey-api/client-fetch && npm run build-openapi-pint", + "build-openapi-local": "rimraf src/api-clients && npx prettier --write ./openapi.json && node_modules/.bin/openapi-ts -i ./openapi.json -o src/api-clients/client -c @hey-api/client-fetch && npm run build-openapi-pint", "build-openapi:staging": "rimraf src/api-clients && curl -o openapi.json https://api.codesandbox.stream/meta/openapi && npx prettier --write ./openapi.json && node_modules/.bin/openapi-ts -i ./openapi.json -o src/api-clients/client -c @hey-api/client-fetch && npm run build-openapi-rest", "build-openapi-rest": "npm run build-openapi-rest-fs && npm run build-openapi-rest-task && npm run build-openapi-rest-container && npm run build-openapi-rest-git && npm run build-openapi-rest-setup && npm run build-openapi-rest-shell && npm run build-openapi-rest-system", "build-openapi-rest-container": "node_modules/.bin/openapi-ts -i ./openapi-sandbox-container.json -o src/api-clients/client-rest-container -c @hey-api/client-fetch", @@ -55,7 +56,7 @@ "build-openapi-pint": "node_modules/.bin/openapi-ts -i ./pint-openapi-bundled.json -o src/api-clients/pint -c @hey-api/client-fetch", "clean": "rimraf ./dist", "test": "vitest", - "test:e2e": "vitest run tests/e2e", + "test:e2e": "vitest run", "typecheck": "tsc --noEmit", "format": "prettier '**/*.{md,js,jsx,json,ts,tsx}' --write", "postbuild": "rimraf {lib,es}/**/__tests__ {lib,es}/**/*.{spec,test}.{js,d.ts,js.map}", diff --git a/pint-openapi-bundled.json b/pint-openapi-bundled.json index a655213..b6da8e4 100644 --- a/pint-openapi-bundled.json +++ b/pint-openapi-bundled.json @@ -1820,6 +1820,116 @@ } } } + }, + "/api/v1/stream/directories/watcher/{path}": { + "get": { + "summary": "Watch directory changes using Server-Sent Events (SSE)", + "tags": [ + "streams", + "files" + ], + "description": "Watch a directory for file system changes and stream events via SSE.", + "operationId": "CreateWatcher", + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "name": "path", + "in": "path", + "required": true, + "description": "Directory path to watch", + "schema": { + "type": "string" + }, + "example": "workspace/src/main.go" + }, + { + "name": "recursive", + "in": "query", + "required": false, + "description": "Whether to watch directories recursively", + "schema": { + "type": "boolean" + }, + "example": true + }, + { + "name": "ignorePatterns", + "in": "query", + "required": false, + "description": "Glob patterns to ignore certain files or directories (can be specified multiple times)", + "schema": { + "type": "array", + "items": { + "type": "string" + } + }, + "style": "form", + "explode": true, + "example": [ + "*.log", + "temp/*", + "node_modules/*" + ] + } + ], + "responses": { + "200": { + "description": "Directory watcher stream started successfully", + "content": { + "text/event-stream": { + "schema": { + "type": "string", + "description": "Server-Sent Events stream of directory files updates" + } + } + } + }, + "400": { + "description": "Bad Request - Path is required or invalid path", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "500": { + "description": "Internal Server Error - Failed to create file", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "default": { + "description": "Unexpected Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + } + } } }, "components": { @@ -2020,6 +2130,10 @@ "type": "boolean", "description": "Whether the exec is interactive" }, + "pty": { + "type": "boolean", + "description": "Whether the exec is using a pty" + }, "exitCode": { "type": "integer", "description": "Exit code of the process (only present when process has exited)" @@ -2032,6 +2146,7 @@ "status", "pid", "interactive", + "pty", "exitCode" ] }, @@ -2071,6 +2186,10 @@ "interactive": { "type": "boolean", "description": "Whether to start interactive shell session or not (defaults to false)" + }, + "pty": { + "type": "boolean", + "description": "Whether to start pty shell session or not (defaults to false)" } }, "required": [ @@ -2105,6 +2224,42 @@ "message" ] }, + "ExecStdout": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Type of the exec output", + "enum": [ + "stdout", + "stderr" + ] + }, + "output": { + "type": "string", + "description": "Data associated with the exec output" + }, + "sequence": { + "type": "integer", + "format": "int32", + "description": "Sequence number of the output message" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "Timestamp of when the output was generated" + }, + "exitCode": { + "type": "integer", + "description": "Exit code of the process (only present when process has exited)" + } + }, + "required": [ + "type", + "output", + "sequence" + ] + }, "ExecStdin": { "type": "object", "properties": { @@ -2400,42 +2555,6 @@ "ports" ] }, - "ExecStdout": { - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Type of the exec output", - "enum": [ - "stdout", - "stderr" - ] - }, - "output": { - "type": "string", - "description": "Data associated with the exec output" - }, - "sequence": { - "type": "integer", - "format": "int32", - "description": "Sequence number of the output message" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "Timestamp of when the output was generated" - }, - "exitCode": { - "type": "integer", - "description": "Exit code of the process (only present when process has exited)" - } - }, - "required": [ - "type", - "output", - "sequence" - ] - }, "Task": { "$ref": "#/components/schemas/TaskItem" } diff --git a/src/API.ts b/src/API.ts index c1fd419..bd04ba8 100644 --- a/src/API.ts +++ b/src/API.ts @@ -336,8 +336,8 @@ export class API { ); return { - bootupType: - handledResponse.bootup_type as PitcherManagerResponse["bootupType"], + bootupType: + handledResponse.bootup_type as PitcherManagerResponse["bootupType"], cluster: handledResponse.cluster, pitcherURL: handledResponse.pitcher_url, workspacePath: handledResponse.workspace_path, @@ -346,6 +346,9 @@ export class API { pitcherVersion: handledResponse.pitcher_version, latestPitcherVersion: handledResponse.latest_pitcher_version, pitcherToken: handledResponse.pitcher_token, + pintToken: handledResponse.pint_token, + pintURL: handledResponse.pint_url, + vmAgentType: handledResponse.vm_agent_type, }; } diff --git a/src/AgentClient/index.ts b/src/AgentClient/index.ts index ddfced6..b59308e 100644 --- a/src/AgentClient/index.ts +++ b/src/AgentClient/index.ts @@ -17,12 +17,15 @@ import { IAgentClientSystem, IAgentClientTasks, PickRawFsResult, + SubscribeShellEvent, } from "../agent-client-interface"; import { AgentConnection } from "./AgentConnection"; import { Emitter, Event } from "../utils/event"; import { DEFAULT_SUBSCRIPTIONS, SandboxSession } from "../types"; import { SandboxClient } from "../SandboxClient"; import { InitStatus } from "../pitcher-protocol/messages/system"; +import { IDisposable } from "@xterm/headless"; +import { Disposable } from "../utils/disposable"; // Timeout for detecting a pong response, leading to a forced disconnect // Increased from 15s to 30s to be more tolerant of network latency @@ -32,46 +35,42 @@ let PONG_DETECTION_TIMEOUT = 30_000; const FOCUS_PONG_DETECTION_TIMEOUT = 5_000; class AgentClientShells implements IAgentClientShells { - private onShellExitedEmitter = new Emitter<{ - shellId: string; - exitCode: number; - }>(); - onShellExited = this.onShellExitedEmitter.event; - - private onShellTerminatedEmitter = new Emitter< - shell.ShellTerminateNotification["params"] - >(); - onShellTerminated = this.onShellTerminatedEmitter.event; - - private onShellOutEmitter = new Emitter< - shell.ShellOutNotification["params"] - >(); - onShellOut = this.onShellOutEmitter.event; - + disposeOutputListener: () => void; + private shellOutputs: Record = {}; constructor(private agentConnection: AgentConnection) { - agentConnection.onNotification("shell/exit", (params) => { - this.onShellExitedEmitter.fire(params); - }); - - agentConnection.onNotification("shell/terminate", (params) => { - this.onShellTerminatedEmitter.fire(params); - }); + // We use a common listener to keep track of all shell output to avoid race conditions. These + // are then flushed. This does not work with multiple listeners, but you would not use multiple + // listeners for command/terminal output anyways. NOTE! These notifications only appear for created/opened shells + this.disposeOutputListener = agentConnection.onNotification( + "shell/out", + (event) => { + if (!this.shellOutputs[event.shellId]) { + this.shellOutputs[event.shellId] = []; + } - agentConnection.onNotification("shell/out", (params) => { - this.onShellOutEmitter.fire(params); - }); + this.shellOutputs[event.shellId].push(event.out); + } + ); } - create( - projectPath: string, - size: shell.ShellSize, - command?: string, - type?: shell.ShellProcessType, - isSystemShell?: boolean - ): Promise { + create({ + command, + args, + size, + type, + isSystemShell, + projectPath, + }: { + command: string; + args: string[]; + projectPath: string; + size: shell.ShellSize; + type?: shell.ShellProcessType; + isSystemShell?: boolean; + }): Promise { return this.agentConnection.request({ method: "shell/create", params: { - command, + command: command + args.join(""), size, type, isSystemShell, @@ -97,17 +96,113 @@ class AgentClientShells implements IAgentClientShells { return result.shells; } - open( + subscribe( shellId: shell.ShellId, - size: shell.ShellSize - ): Promise { - return this.agentConnection.request({ - method: "shell/open", - params: { - shellId, - size, - }, + listener: (event: SubscribeShellEvent) => void + ): IDisposable { + const disposable = new Disposable(); + + const disposeExit = this.agentConnection.onNotification( + "shell/exit", + (params) => { + if (params.shellId === shellId) { + listener({ type: "exit", exitCode: params.exitCode }); + } + } + ); + + const disposeTerminate = this.agentConnection.onNotification( + "shell/terminate", + (params) => { + if (params.shellId === shellId) { + listener({ type: "terminate" }); + } + } + ); + + disposable.onDidDispose(() => { + disposeExit(); + disposeTerminate(); + }); + + return disposable; + } + subscribeOutput( + shellId: shell.ShellId, + size: shell.ShellSize, + listener: (event: { out: string; exitCode?: number }) => void + ): IDisposable { + const disposable = new Disposable(); + let disposeOut: () => void; + let disposeExit: () => void; + + this.agentConnection + .request({ + method: "shell/open", + params: { + shellId, + size, + }, + }) + .then((openShell) => { + listener({ + out: openShell.buffer.join("\n"), + exitCode: openShell.exitCode, + }); + + if (typeof openShell.exitCode === "number") { + return; + } + + disposeOut = this.agentConnection.onNotification( + "shell/out", + (params) => { + if (params.shellId === shellId) { + listener({ out: params.out, exitCode: openShell.exitCode }); + } + } + ); + disposeExit = this.agentConnection.onNotification( + "shell/exit", + (params) => { + if (params.shellId === shellId) { + listener({ out: "", exitCode: params.exitCode }); + } + } + ); + }) + .catch(() => { + // Pitcher requires a global shell listener for output to avoid race conditions. When running commands the shell can close + // before we get the output, so this just flushes the output gotten in between creating and subscribing + listener({ + out: this.shellOutputs[shellId] + ? this.shellOutputs[shellId].join("") + : "", + // We give a fake exit code, because pint gives an exit code on last event... but we do not know the exit code as the + // shell is already gone + exitCode: -1, + }); + this.shellOutputs[shellId].length = 0; + }); + + disposable.onDidDispose(() => { + this.agentConnection + .request({ + method: "shell/close", + params: { + shellId, + size, + }, + }) + .catch(() => { + // We do not care + }); + + disposeOut?.(); + disposeExit?.(); }); + + return disposable; } rename(shellId: shell.ShellId, name: string): Promise { return this.agentConnection.request({ @@ -390,6 +485,7 @@ class AgentClientSystem implements IAgentClientSystem { } export class AgentClient implements IAgentClient { + readonly type = "pitcher" as const; static async create({ session, getSession, @@ -483,6 +579,7 @@ export class AgentClient implements IAgentClient { } } dispose() { + this.shells.disposeOutputListener(); this.agentConnection.dispose(); } } diff --git a/src/PintClient/execs.ts b/src/PintClient/execs.ts index eaa8e5b..3f8e5b7 100644 --- a/src/PintClient/execs.ts +++ b/src/PintClient/execs.ts @@ -3,7 +3,8 @@ import { Emitter, EmitterSubscription } from "../utils/event"; import { Disposable } from "../utils/disposable"; import { parseStreamEvent } from "./utils"; import { - IAgentClientShells, + IAgentClientShells, + SubscribeShellEvent, } from "../agent-client-interface"; import { createExec, @@ -27,17 +28,19 @@ import { ShellDTO, ShellProcessStatus, } from "../pitcher-protocol/messages/shell"; +import { IDisposable } from "@xterm/headless"; export class PintShellsClient implements IAgentClientShells { - private openShells: Record = {}; + private execs: ExecItem[] = []; + constructor(private apiClient: Client, private sandboxId: string) {} private subscribeAndEvaluateExecsUpdates( + execId: string, compare: ( nextExec: ExecItem, prevExec: ExecItem | undefined, prevExecs: ExecItem[] ) => void ) { - let prevExecs: ExecItem[] = []; const abortController = new AbortController(); streamExecsList({ @@ -51,17 +54,19 @@ export class PintShellsClient implements IAgentClientShells { const execListResponse = parseStreamEvent(evt); const execs = execListResponse.execs; - if (prevExecs && execs) { - execs.forEach((exec) => { - const prevExec = prevExecs?.find( - (execItem) => execItem.id === exec.id - ); + execs.forEach((exec) => { + if (exec.id !== execId) { + return; + } + + const prevExec = this.execs.find( + (execItem) => execItem.id === exec.id + ); - compare(exec, prevExec, prevExecs); - }); - } + compare(exec, prevExec, this.execs); + }); - prevExecs = execs || []; + this.execs = execs; } }); @@ -69,49 +74,6 @@ export class PintShellsClient implements IAgentClientShells { abortController.abort(); }); } - private onShellExitedEmitter = new EmitterSubscription<{ - shellId: string; - exitCode: number; - }>((fire) => - this.subscribeAndEvaluateExecsUpdates((exec, prevExec) => { - if (!prevExec) { - return; - } - - if (prevExec.status === "RUNNING" && exec.status === "EXITED") { - fire({ - shellId: exec.id, - exitCode: exec.exitCode, - }); - } - }) - ); - onShellExited = this.onShellExitedEmitter.event; - - private onShellOutEmitter = new Emitter<{ - shellId: ShellId; - out: string; - }>(); - onShellOut = this.onShellOutEmitter.event; - private onShellTerminatedEmitter = new EmitterSubscription<{ - shellId: string; - author: string; - }>((fire) => - this.subscribeAndEvaluateExecsUpdates((exec, prevExec) => { - if (!prevExec) { - return; - } - - if (prevExec.status === "RUNNING" && exec.status === "STOPPED") { - fire({ - shellId: exec.id, - author: "", - }); - } - }) - ); - onShellTerminated = this.onShellTerminatedEmitter.event; - constructor(private apiClient: Client, private sandboxId: string) {} private convertExecToShellDTO(exec: ExecItem) { return { isSystemShell: true, @@ -127,21 +89,25 @@ export class PintShellsClient implements IAgentClientShells { status: exec.status as ShellProcessStatus, }; } - async create( - projectPath: string, - size: ShellSize, - command?: string, - type?: ShellProcessType, - isSystemShell?: boolean - ): Promise { - // For Pint, we need to construct args from command - const args = command ? command.split(' ').slice(1) : []; - const baseCommand = command ? command.split(' ')[0] : 'bash'; + async create({ + command, + args, + projectPath, + size, + type, + }: { + command: string; + args: string[]; + projectPath: string; + size: ShellSize; + type?: ShellProcessType; + isSystemShell?: boolean; + }): Promise { const exec = await createExec({ client: this.apiClient, body: { args, - command: baseCommand, + command, interactive: type === "COMMAND" ? false : true, }, }); @@ -150,14 +116,69 @@ export class PintShellsClient implements IAgentClientShells { throw new Error(exec.error.message); } - await this.open(exec.data.id, { cols: 200, rows: 80 }); + this.execs.push(exec.data); return { ...this.convertExecToShellDTO(exec.data), buffer: [], }; } - async delete(shellId: ShellId): Promise { + subscribe( + shellId: ShellId, + listener: (event: SubscribeShellEvent) => void + ): IDisposable { + return this.subscribeAndEvaluateExecsUpdates(shellId, (exec, prevExec) => { + if (!prevExec) { + return; + } + + if (prevExec.status === "RUNNING" && exec.status === "EXITED") { + listener({ + type: "exit", + exitCode: exec.exitCode, + }); + } + }); + } + subscribeOutput( + shellId: ShellId, + size: ShellSize, + listener: (event: { out: string; exitCode?: number }) => void + ): IDisposable { + const disposable = new Disposable(); + const abortController = new AbortController(); + + getExecOutput({ + client: this.apiClient, + path: { id: shellId }, + query: { lastSequence: 0 }, + signal: abortController.signal, + headers: { + Accept: "text/event-stream", + }, + }).then(async ({ stream }) => { + for await (const evt of stream) { + const data = parseStreamEvent<{ + type: "stdout" | "stderr"; + output: ""; + sequence: number; + timestamp: string; + exitCode?: number; + }>(evt); + + listener({ out: data.output, exitCode: data.exitCode }); + } + }); + + disposable.onDidDispose(() => { + abortController.abort(); + }); + + return disposable; + } + async delete( + shellId: ShellId + ): Promise { try { // First get the exec details before deleting it const exec = await getExec({ @@ -183,12 +204,6 @@ export class PintShellsClient implements IAgentClientShells { }); if (deleteResponse.data) { - // Clean up any open shells reference - if (this.openShells[shellId]) { - this.openShells[shellId].abort(); - delete this.openShells[shellId]; - } - return shellDTO as CommandShellDTO | TerminalShellDTO; } else { return null; @@ -206,53 +221,6 @@ export class PintShellsClient implements IAgentClientShells { execs.data?.execs.map((exec) => this.convertExecToShellDTO(exec)) ?? [] ); } - async open(shellId: ShellId, size: ShellSize): Promise { - const abortController = new AbortController(); - - this.openShells[shellId] = abortController; - - const exec = await getExec({ - client: this.apiClient, - path: { - id: shellId, - }, - }); - - if (!exec.data) { - throw new Error(exec.error.message); - } - - const { stream } = await getExecOutput({ - client: this.apiClient, - path: { id: shellId }, - query: { lastSequence: 0 }, - signal: abortController.signal, - headers: { - Accept: "text/event-stream", - }, - }); - - const buffer: string[] = []; - - for await (const evt of stream) { - const data = parseStreamEvent<{ - type: "stdout" | "stderr"; - output: ""; - sequence: number; - timestamp: string; - }>(evt); - - if (!buffer.length) { - buffer.push(data.output); - break; - } - } - - return { - buffer, - ...this.convertExecToShellDTO(exec.data), - }; - } async rename(shellId: ShellId, name: string): Promise { return null; } @@ -264,7 +232,7 @@ export class PintShellsClient implements IAgentClientShells { id: shellId, }, body: { - status: 'running', + status: "running", }, }); @@ -281,7 +249,7 @@ export class PintShellsClient implements IAgentClientShells { id: shellId, }, body: { - type: 'stdin', + type: "stdin", input: input, }, }); diff --git a/src/PintClient/fs.ts b/src/PintClient/fs.ts index e1392bc..99076ed 100644 --- a/src/PintClient/fs.ts +++ b/src/PintClient/fs.ts @@ -3,6 +3,9 @@ import { IAgentClientFS, PickRawFsResult, } from "../agent-client-interface"; +import { fs } from "../pitcher-protocol"; +import { Disposable } from "../utils/disposable"; +import { parseStreamEvent } from "./utils"; import { createFile, readFile, @@ -11,6 +14,7 @@ import { createDirectory, deleteDirectory, getFileStat, + createWatcher, } from "../api-clients/pint"; export class PintFsClient implements IAgentClientFS { constructor(private apiClient: Client) {} @@ -325,12 +329,55 @@ export class PintFsClient implements IAgentClientFS { readonly recursive?: boolean; readonly excludes?: readonly string[]; }, - onEvent: (watchEvent: any) => void + onEvent: (watchEvent: fs.FSWatchEvent) => void ): Promise< | (PickRawFsResult<"fs/watch"> & { type: "error" }) | { type: "success"; dispose(): void } > { - throw new Error("Not implemented"); + try { + const abortController = new AbortController(); + + const response = await createWatcher({ + client: this.apiClient, + path: { + path: path, + }, + query: { + recursive: options.recursive, + ignorePatterns: options.excludes ? [...options.excludes] : undefined, + }, + signal: abortController.signal, + }); + + // Start listening to the stream in the background + (async () => { + try { + for await (const evt of response.stream) { + try { + const watchEvent = parseStreamEvent(evt); + onEvent(watchEvent); + } catch (error) { + console.warn('Failed to parse filesystem watch event:', error); + } + } + } catch (error) { + console.error('Filesystem watch stream error:', error); + } + })(); + + return { + type: "success", + dispose(): void { + abortController.abort(); + }, + }; + } catch (error) { + return { + type: "error", + error: error instanceof Error ? error.message : "Unknown error", + errno: null, + }; + } } async download(path?: string): Promise<{ downloadUrl: string }> { diff --git a/src/Sandbox.ts b/src/Sandbox.ts index cc4ad7f..7cc83be 100644 --- a/src/Sandbox.ts +++ b/src/Sandbox.ts @@ -151,13 +151,13 @@ export class Sandbox { return `export ${key}='${safe}'`; }) .join("\n"); - commands.push( - [ - `cat << 'EOF' > "$HOME/.private/.env"`, - envStrings, - `EOF`, - ].join("\n") - ); + const cmd = [ + `mkdir -p "$HOME/.private"`, + `cat << 'EOF' > "$HOME/.private/.env"`, + envStrings, + `EOF`, + ].join("\n"); + await client.commands.run(cmd); } if (customSession.git) { @@ -188,8 +188,7 @@ export class Sandbox { pitcherManagerResponse: PitcherManagerResponse, customSession?: SessionCreateOptions ): Promise { - // HACK: we currently do not get a flag for pint, but this is a check we can use for now - const isPint = false; + const isPint = pitcherManagerResponse.vmAgentType === "pint"; if (!customSession || !customSession.id) { return { @@ -205,6 +204,9 @@ export class Sandbox { userWorkspacePath: pitcherManagerResponse.userWorkspacePath, workspacePath: pitcherManagerResponse.workspacePath, pitcherVersion: pitcherManagerResponse.pitcherVersion, + pintToken: pitcherManagerResponse.pintToken, + pintURL: pitcherManagerResponse.pintURL, + vmAgentType: pitcherManagerResponse.vmAgentType, }; } @@ -231,6 +233,9 @@ export class Sandbox { userWorkspacePath: handledResponse.user_workspace_path, workspacePath: pitcherManagerResponse.workspacePath, pitcherVersion: pitcherManagerResponse.pitcherVersion, + pintToken: pitcherManagerResponse.pintToken, + pintURL: pitcherManagerResponse.pintURL, + vmAgentType: pitcherManagerResponse.vmAgentType, }; } diff --git a/src/SandboxClient/commands.ts b/src/SandboxClient/commands.ts index 50b842a..722c59f 100644 --- a/src/SandboxClient/commands.ts +++ b/src/SandboxClient/commands.ts @@ -104,6 +104,79 @@ export class SandboxCommands { ); } + private async runBackgroundPitcher( + command: string | string[], + opts?: ShellRunOpts + ) { + const disposableStore = new DisposableStore(); + const onOutput = new Emitter(); + disposableStore.add(onOutput); + + command = Array.isArray(command) ? command.join(" && ") : command; + + const passedEnv = Object.assign(opts?.env ?? {}); + + const escapedCommand = command.replace(/'/g, "'\\''"); + + // TODO: use a new shell API that natively supports cwd & env + let commandWithEnv = Object.keys(passedEnv).length + ? `source $HOME/.private/.env 2>/dev/null || true && env ${Object.entries( + passedEnv + ) + .map(([key, value]) => { + const escapedValue = String(value).replace(/'/g, "'\\''"); + return `${key}='${escapedValue}'`; + }) + .join(" ")} bash -c '${escapedCommand}'` + : `source $HOME/.private/.env 2>/dev/null || true && bash -c '${escapedCommand}'`; + + if (opts?.cwd) { + commandWithEnv = `cd ${opts.cwd} && ${commandWithEnv}`; + } + + const shell = await this.agentClient.shells.create({ + projectPath: this.agentClient.workspacePath, + size: opts?.dimensions ?? DEFAULT_SHELL_SIZE, + command: commandWithEnv, + args: [], + type: opts?.asGlobalSession ? "COMMAND" : "TERMINAL", + isSystemShell: true, + }); + + if (shell.status === "ERROR" || shell.status === "KILLED") { + throw new Error(`Failed to create shell: ${shell.buffer.join("\n")}`); + } + + const details = { + type: "command", + command, + name: opts?.name, + }; + + if (shell.status !== "FINISHED") { + // Only way for us to differentiate between a command and a terminal + this.agentClient.shells + .rename( + shell.shellId, + // We embed some details in the name to properly show the command that was run + // , the name and that it is an actual command + JSON.stringify(details) + ) + .catch(() => { + // It is already done + }); + } + + const cmd = new Command( + this.agentClient, + shell as protocol.shell.CommandShellDTO, + details, + this.tracer + ); + + return cmd; + } + /** * Create and run command in a new shell. Allows you to listen to the output and kill the command. */ @@ -118,39 +191,37 @@ export class SandboxCommands { "command.name": opts?.name || "", }, async () => { - const disposableStore = new DisposableStore(); - const onOutput = new Emitter(); - disposableStore.add(onOutput); + if (this.agentClient.type === "pitcher") { + return this.runBackgroundPitcher(command, opts); + } command = Array.isArray(command) ? command.join(" && ") : command; const passedEnv = Object.assign(opts?.env ?? {}); - const escapedCommand = command.replace(/'/g, "'\\''"); + // Build bash args array + const args = ["source $HOME/.private/.env 2>/dev/null || true"]; - // TODO: use a new shell API that natively supports cwd & env - let commandWithEnv = Object.keys(passedEnv).length - ? `source $HOME/.private/.env 2>/dev/null || true && env ${Object.entries( - passedEnv - ) - .map(([key, value]) => { - const escapedValue = String(value).replace(/'/g, "'\\''"); - return `${key}='${escapedValue}'`; - }) - .join(" ")} bash -c '${escapedCommand}'` - : `source $HOME/.private/.env 2>/dev/null || true && bash -c '${escapedCommand}'`; + if (Object.keys(passedEnv).length) { + Object.entries(passedEnv).forEach(([key, value]) => { + args.push("&&", "env", `${key}=${value}`); + }); + } if (opts?.cwd) { - commandWithEnv = `cd ${opts.cwd} && ${commandWithEnv}`; + args.push("&&", "cd", opts.cwd); } - const shell = await this.agentClient.shells.create( - this.agentClient.workspacePath, - opts?.dimensions ?? DEFAULT_SHELL_SIZE, - commandWithEnv, - opts?.asGlobalSession ? "COMMAND" : "TERMINAL", - true - ); + args.push("&&", command); + + const shell = await this.agentClient.shells.create({ + command: "bash", + args: ["-c", args.join(" ")], + projectPath: this.agentClient.workspacePath, + size: opts?.dimensions ?? DEFAULT_SHELL_SIZE, + type: opts?.asGlobalSession ? "COMMAND" : "TERMINAL", + isSystemShell: true, + }); if (shell.status === "ERROR" || shell.status === "KILLED") { throw new Error(`Failed to create shell: ${shell.buffer.join("\n")}`); @@ -216,7 +287,8 @@ export class SandboxCommands { return shells .filter( - (shell) => shell.shellType === "TERMINAL" && isCommandShell(shell) + (shell): shell is protocol.shell.CommandShellDTO => + shell.shellType === "TERMINAL" && isCommandShell(shell) ) .map( (shell) => @@ -266,6 +338,7 @@ export class Command { private barrier = new Barrier(); private output: string[] = []; + private isSubscribingOutput = false; /** * The status of the command. @@ -308,39 +381,32 @@ export class Command { this.name = details.name; this.tracer = tracer; - this.disposable.addDisposable( - agentClient.shells.onShellExited(({ shellId, exitCode }) => { - if (shellId === this.shell.shellId) { - this.exitCode = exitCode; - this.status = exitCode === 0 ? "FINISHED" : "ERROR"; - this.barrier.open(); - } - }) - ); - - this.disposable.addDisposable( - agentClient.shells.onShellTerminated(({ shellId }) => { - if (shellId === this.shell.shellId) { - this.status = "KILLED"; - this.barrier.open(); - } - }) - ); - - this.disposable.addDisposable( - this.agentClient.shells.onShellOut(({ shellId, out }) => { - if (shellId !== this.shell.shellId || out.startsWith("[CODESANDBOX]")) { - return; - } - - this.onOutputEmitter.fire(out); - - this.output.push(out); - if (this.output.length > 1000) { - this.output.shift(); - } - }) - ); + if (shell.status === "RUNNING") { + this.disposable.addDisposable( + agentClient.shells.subscribe(shell.shellId, async (event) => { + if (event.type === "terminate") { + this.status = "KILLED"; + this.barrier.open(); + } else { + const barrier = new Barrier(); + this.agentClient.shells.subscribeOutput( + this.shell.shellId, + DEFAULT_SHELL_SIZE, + (event) => { + this.output.push(event.out); + if (event.exitCode !== undefined) { + barrier.open(); + } + } + ); + await barrier.wait(); + this.exitCode = event.exitCode; + this.status = event.exitCode === 0 ? "FINISHED" : "ERROR"; + this.barrier.open(); + } + }) + ); + } } private async withSpan( @@ -389,14 +455,45 @@ export class Command { "command.dimensions.rows": dimensions.rows, }, async () => { - const shell = await this.agentClient.shells.open( - this.shell.shellId, - dimensions + if (this.isSubscribingOutput) { + return this.output.join("\n"); + } + + this.isSubscribingOutput = true; + const barrier = new Barrier(); + + this.disposable.addDisposable( + this.agentClient.shells.subscribeOutput( + this.shell.shellId, + dimensions, + ({ out }) => { + if (barrier.isOpen()) { + this.onOutputEmitter.fire(out); + + this.output.push(out); + if (this.output.length > 1000) { + this.output.shift(); + } + } else { + this.output.push(out); + barrier.open(out); + } + } + ) ); - this.output = shell.buffer; + this.disposable.onDidDispose(() => { + barrier.dispose(); + }); + + const result = await barrier.wait(); + + // This will never really happen + if (result.status === "disposed") { + return ""; + } - return this.output.join("\n"); + return result.value; } ); } diff --git a/src/SandboxClient/filesystem.ts b/src/SandboxClient/filesystem.ts index 68440a5..cb33ef4 100644 --- a/src/SandboxClient/filesystem.ts +++ b/src/SandboxClient/filesystem.ts @@ -184,13 +184,16 @@ export class FileSystem { try { // Extract the zip file using unzip command - const result = await this.agentClient.shells.create( - this.agentClient.workspacePath, - { cols: 128, rows: 24 }, - `cd ${this.agentClient.workspacePath} && unzip -o ${tempZipPath}`, - "COMMAND", - true - ); + const result = await this.agentClient.shells.create({ + projectPath: this.agentClient.workspacePath, + size: { cols: 128, rows: 24 }, + command: "bash", + args: [ + `cd ${this.agentClient.workspacePath} && unzip -o ${tempZipPath}`, + ], + type: "COMMAND", + isSystemShell: true, + }); if (result.status === "ERROR" || result.status === "KILLED") { throw new Error( @@ -204,16 +207,17 @@ export class FileSystem { if (result.status === "RUNNING") { // Wait for shell exit event await new Promise((resolve, reject) => { - const disposable = this.agentClient.shells.onShellExited( - ({ shellId, exitCode }) => { - if (shellId === result.shellId) { + const disposable = this.agentClient.shells.subscribe( + result.shellId, + (event) => { + if (event.type === "exit") { disposable.dispose(); - if (exitCode === 0) { + if (event.exitCode === 0) { resolve(); } else { reject( new Error( - `Unzip command failed with exit code ${exitCode}` + `Unzip command failed with exit code ${event.exitCode}` ) ); } diff --git a/src/SandboxClient/setup.ts b/src/SandboxClient/setup.ts index 5b5d36e..8ecdf54 100644 --- a/src/SandboxClient/setup.ts +++ b/src/SandboxClient/setup.ts @@ -4,6 +4,7 @@ import { Emitter } from "../utils/event"; import { DEFAULT_SHELL_SIZE } from "./terminals"; import { type IAgentClient } from "../agent-client-interface"; import { Tracer, SpanStatusCode } from "@opentelemetry/api"; +import { Barrier } from "../utils/barrier"; export class Setup { private disposable = new Disposable(); @@ -164,18 +165,6 @@ export class Step { } }) ); - this.disposable.addDisposable( - this.agentClient.shells.onShellOut(({ shellId, out }) => { - if (shellId === this.step.shellId) { - this.onOutputEmitter.fire(out); - - this.output.push(out); - if (this.output.length > 1000) { - this.output.shift(); - } - } - }) - ); } private withSpan( @@ -224,11 +213,31 @@ export class Step { }, async () => { const open = async (shellId: protocol.shell.ShellId) => { - const shell = await this.agentClient.shells.open(shellId, dimensions); + const barrier = new Barrier(); + this.agentClient.shells.subscribeOutput( + shellId, + dimensions, + ({ out }) => { + if (barrier.isOpen()) { + this.onOutputEmitter.fire(out); - this.output = shell.buffer; + this.output.push(out); + if (this.output.length > 1000) { + this.output.shift(); + } + } else { + this.output.push(out); + barrier.open(out); + } + } + ); + const result = await barrier.wait(); + + if (result.status === "disposed") { + return ""; + } - return this.output.join("\n"); + return result.value; }; if (this.step.shellId) { diff --git a/src/SandboxClient/tasks.ts b/src/SandboxClient/tasks.ts index 70d7a8e..2f6928a 100644 --- a/src/SandboxClient/tasks.ts +++ b/src/SandboxClient/tasks.ts @@ -107,6 +107,7 @@ export class Task { output: string[]; dimensions: typeof DEFAULT_SHELL_SIZE; }; + private currentSubscribeOutput?: IDisposable; private onOutputEmitter = this.disposable.addDisposable( new Emitter() ); @@ -174,36 +175,19 @@ export class Task { task.shell && task.shell.shellId !== lastShellId ) { - const openedShell = await this.agentClient.shells.open( + const openedShell = this.openedShell; + this.currentSubscribeOutput?.dispose(); + this.openedShell.shellId = task.shell.shellId; + this.currentSubscribeOutput = this.agentClient.shells.subscribeOutput( task.shell.shellId, - this.openedShell.dimensions + this.openedShell.dimensions, + ({ out }) => { + this.onOutputEmitter.fire("\x1B[2J\x1B[3J\x1B[1;1H"); + openedShell.output.push(out); + this.onOutputEmitter.fire(out); + } ); - - this.openedShell = { - shellId: openedShell.shellId, - output: openedShell.buffer, - dimensions: this.openedShell.dimensions, - }; - - this.onOutputEmitter.fire("\x1B[2J\x1B[3J\x1B[1;1H"); - openedShell.buffer.forEach((out) => this.onOutputEmitter.fire(out)); - } - }) - ); - - this.disposable.addDisposable( - this.agentClient.shells.onShellOut(({ shellId, out }) => { - if ( - !this.shell || - this.shell.shellId !== shellId || - !this.openedShell - ) { - return; } - - // Update output for shell - this.openedShell.output.push(out); - this.onOutputEmitter.fire(out); }) ); } @@ -256,16 +240,24 @@ export class Task { throw new Error("Task is not running"); } - const openedShell = await this.agentClient.shells.open( - this.shell.shellId, - dimensions - ); + if (this.openedShell) { + return this.openedShell.output.join("\n"); + } - this.openedShell = { - shellId: openedShell.shellId, - output: openedShell.buffer, + const openedShell = (this.openedShell = { dimensions, - }; + output: [] as string[], + shellId: this.shell.shellId, + }); + + this.currentSubscribeOutput = this.agentClient.shells.subscribeOutput( + this.shell.shellId, + dimensions, + ({ out }) => { + openedShell.output.push(out); + this.onOutputEmitter.fire(out); + } + ); return this.openedShell.output.join("\n"); } @@ -356,6 +348,7 @@ export class Task { ); } dispose() { + this.currentSubscribeOutput?.dispose(); this.disposable.dispose(); } } diff --git a/src/SandboxClient/terminals.ts b/src/SandboxClient/terminals.ts index 3310b9c..ed6ab2c 100644 --- a/src/SandboxClient/terminals.ts +++ b/src/SandboxClient/terminals.ts @@ -4,6 +4,7 @@ import { Emitter } from "../utils/event"; import { isCommandShell, ShellRunOpts } from "./commands"; import { type IAgentClient } from "../agent-client-interface"; import { Tracer, SpanStatusCode } from "@opentelemetry/api"; +import { Barrier } from "../utils/barrier"; export type ShellSize = { cols: number; rows: number }; @@ -57,6 +58,41 @@ export class Terminals { ); } + private async createPitcherTerminal( + command: "bash" | "zsh" | "fish" | "ksh" | "dash" = "bash", + opts?: ShellRunOpts + ) { + const allEnv = Object.assign(opts?.env ?? {}); + + // TODO: use a new shell API that natively supports cwd & env + let commandWithEnv = Object.keys(allEnv).length + ? `source $HOME/.private/.env 2>/dev/null || true && env ${Object.entries( + allEnv + ) + .map(([key, value]) => `${key}=${value}`) + .join(" ")} ${command}` + : `source $HOME/.private/.env 2>/dev/null || true && ${command}`; + + if (opts?.cwd) { + commandWithEnv = `cd ${opts.cwd} && ${commandWithEnv}`; + } + + const shell = await this.agentClient.shells.create({ + projectPath: this.agentClient.workspacePath, + size: opts?.dimensions ?? DEFAULT_SHELL_SIZE, + command: commandWithEnv, + args: [], + type: "TERMINAL", + isSystemShell: true, + }); + + if (opts?.name) { + this.agentClient.shells.rename(shell.shellId, opts.name); + } + + return new Terminal(shell, this.agentClient, this.tracer); + } + async create( command: "bash" | "zsh" | "fish" | "ksh" | "dash" = "bash", opts?: ShellRunOpts @@ -71,34 +107,41 @@ export class Terminals { hasDimensions: !!opts?.dimensions, }, async () => { - const allEnv = Object.assign(opts?.env ?? {}); + if (this.agentClient.type === "pitcher") { + return this.createPitcherTerminal(command, opts); + } + + const passedEnv = Object.assign(opts?.env ?? {}); - // TODO: use a new shell API that natively supports cwd & env - let commandWithEnv = Object.keys(allEnv).length - ? `source $HOME/.private/.env 2>/dev/null || true && env ${Object.entries( - allEnv - ) - .map(([key, value]) => `${key}=${value}`) - .join(" ")} ${command}` - : `source $HOME/.private/.env 2>/dev/null || true && ${command}`; + // Build bash args array + const args = ["source $HOME/.private/.env 2>/dev/null || true"]; + + if (Object.keys(passedEnv).length) { + Object.entries(passedEnv).forEach(([key, value]) => { + args.push("&&", "env", `${key}=${value}`); + }); + } if (opts?.cwd) { - commandWithEnv = `cd ${opts.cwd} && ${commandWithEnv}`; + args.push("&&", "cd", opts.cwd); } - const shell = await this.agentClient.shells.create( - this.agentClient.workspacePath, - opts?.dimensions ?? DEFAULT_SHELL_SIZE, - commandWithEnv, - "TERMINAL", - true - ); + const shell = await this.agentClient.shells.create({ + projectPath: this.agentClient.workspacePath, + size: opts?.dimensions ?? DEFAULT_SHELL_SIZE, + command, + args: this.agentClient.type === "pint" ? [] : args, + type: "TERMINAL", + isSystemShell: true, + }); - if (opts?.name) { - this.agentClient.shells.rename(shell.shellId, opts.name); + const terminal = new Terminal(shell, this.agentClient, this.tracer); + + if (this.agentClient.type === "pint") { + await terminal.write(args.join(" ") + "\n"); } - return new Terminal(shell, this.agentClient, this.tracer); + return terminal; } ); } @@ -143,6 +186,7 @@ export class Terminal { ); public readonly onOutput = this.onOutputEmitter.event; private output = this.shell.buffer || []; + private isSubscribingOutput = false; /** * Gets the ID of the terminal. Can be used to open it again. @@ -164,18 +208,6 @@ export class Terminal { tracer?: Tracer ) { this.tracer = tracer; - this.disposable.addDisposable( - this.agentClient.shells.onShellOut(({ shellId, out }) => { - if (shellId === this.shell.shellId) { - this.onOutputEmitter.fire(out); - - this.output.push(out); - if (this.output.length > 1000) { - this.output.shift(); - } - } - }) - ); } private async withSpan( @@ -223,14 +255,44 @@ export class Terminal { rows: dimensions.rows, }, async () => { - const shell = await this.agentClient.shells.open( - this.shell.shellId, - dimensions + if (this.isSubscribingOutput) { + return this.output.join("\n"); + } + + const barrier = new Barrier(); + + this.disposable.addDisposable( + this.agentClient.shells.subscribeOutput( + this.shell.shellId, + dimensions, + ({ out }) => { + if (barrier.isOpen()) { + this.onOutputEmitter.fire(out); + + this.output.push(out); + if (this.output.length > 1000) { + this.output.shift(); + } + } else { + this.output.push(out); + barrier.open(out); + } + } + ) ); - this.output = shell.buffer; + this.disposable.onDidDispose(() => { + barrier.dispose(); + }); + + const result = await barrier.wait(); + + // This will never really happen + if (result.status === "disposed") { + return ""; + } - return this.output.join("\n"); + return result.value; } ); } diff --git a/src/agent-client-interface.ts b/src/agent-client-interface.ts index a5f9a55..2bcc33d 100644 --- a/src/agent-client-interface.ts +++ b/src/agent-client-interface.ts @@ -1,3 +1,4 @@ +import { IDisposable } from "@xterm/headless"; import { fs, port, @@ -11,26 +12,35 @@ import { } from "./pitcher-protocol"; import { Event } from "./utils/event"; +export type SubscribeShellEvent = + | { + type: "exit"; + exitCode: number; + } + | { + type: "terminate"; + }; + export interface IAgentClientShells { - onShellExited: Event<{ - shellId: string; - exitCode: number; - }>; - onShellTerminated: Event; - onShellOut: Event; - create( - projectPath: string, - size: shell.ShellSize, - command?: string, - type?: shell.ShellProcessType, - isSystemShell?: boolean - ): Promise; + create(options: { + command: string; + args: string[]; + projectPath: string; + size: shell.ShellSize; + type?: shell.ShellProcessType; + isSystemShell?: boolean; + }): Promise; rename(shellId: shell.ShellId, name: string): Promise; getShells(): Promise; - open( + subscribe( shellId: shell.ShellId, - size: shell.ShellSize - ): Promise; + listener: (event: SubscribeShellEvent) => void + ): IDisposable; + subscribeOutput( + shellId: shell.ShellId, + size: shell.ShellSize, + listener: (event: { out: string; exitCode?: number }) => void + ): IDisposable; delete( shellId: shell.ShellId ): Promise; @@ -128,6 +138,7 @@ export type IAgentClientState = | "HIBERNATED"; export interface IAgentClient { + type: "pitcher" | "pint"; sandboxId: string; workspacePath: string; isUpToDate: boolean; diff --git a/src/api-clients/client/types.gen.ts b/src/api-clients/client/types.gen.ts index e30ac60..4f1cad1 100644 --- a/src/api-clients/client/types.gen.ts +++ b/src/api-clients/client/types.gen.ts @@ -599,6 +599,7 @@ export type VmStartResponse = { reconnect_token: string; use_pint: boolean; user_workspace_path: string; + vm_agent_type: string; workspace_path: string; }; }; @@ -964,6 +965,7 @@ export type SandboxForkResponse = { reconnect_token: string; use_pint: boolean; user_workspace_path: string; + vm_agent_type: string; workspace_path: string; } | null; title: string | null; diff --git a/src/api-clients/pint/sdk.gen.ts b/src/api-clients/pint/sdk.gen.ts index 89545b6..a3b14e9 100644 --- a/src/api-clients/pint/sdk.gen.ts +++ b/src/api-clients/pint/sdk.gen.ts @@ -2,7 +2,7 @@ import type { Client, Options as Options2, TDataShape } from './client'; import { client } from './client.gen'; -import type { ConnectToExecWebSocketData, ConnectToExecWebSocketErrors, ConnectToExecWebSocketResponses, CreateDirectoryData, CreateDirectoryErrors, CreateDirectoryResponses, CreateExecData, CreateExecErrors, CreateExecResponses, CreateFileData, CreateFileErrors, CreateFileResponses, DeleteDirectoryData, DeleteDirectoryErrors, DeleteDirectoryResponses, DeleteExecData, DeleteExecErrors, DeleteExecResponses, DeleteFileData, DeleteFileErrors, DeleteFileResponses, ExecExecStdinData, ExecExecStdinErrors, ExecExecStdinResponses, ExecuteTaskActionData, ExecuteTaskActionErrors, ExecuteTaskActionResponses, GetExecData, GetExecErrors, GetExecOutputData, GetExecOutputErrors, GetExecOutputResponses, GetExecResponses, GetFileStatData, GetFileStatErrors, GetFileStatResponses, GetTaskData, GetTaskErrors, GetTaskResponses, ListDirectoryData, ListDirectoryErrors, ListDirectoryResponses, ListExecsData, ListExecsErrors, ListExecsResponses, ListPortsData, ListPortsErrors, ListPortsResponses, ListSetupTasksData, ListSetupTasksErrors, ListSetupTasksResponses, ListTasksData, ListTasksErrors, ListTasksResponses, PerformFileActionData, PerformFileActionErrors, PerformFileActionResponses, ReadFileData, ReadFileErrors, ReadFileResponses, StreamExecsListData, StreamExecsListErrors, StreamExecsListResponses, StreamPortsListData, StreamPortsListErrors, StreamPortsListResponses, UpdateExecData, UpdateExecErrors, UpdateExecResponses } from './types.gen'; +import type { ConnectToExecWebSocketData, ConnectToExecWebSocketErrors, ConnectToExecWebSocketResponses, CreateDirectoryData, CreateDirectoryErrors, CreateDirectoryResponses, CreateExecData, CreateExecErrors, CreateExecResponses, CreateFileData, CreateFileErrors, CreateFileResponses, CreateWatcherData, CreateWatcherErrors, CreateWatcherResponses, DeleteDirectoryData, DeleteDirectoryErrors, DeleteDirectoryResponses, DeleteExecData, DeleteExecErrors, DeleteExecResponses, DeleteFileData, DeleteFileErrors, DeleteFileResponses, ExecExecStdinData, ExecExecStdinErrors, ExecExecStdinResponses, ExecuteTaskActionData, ExecuteTaskActionErrors, ExecuteTaskActionResponses, GetExecData, GetExecErrors, GetExecOutputData, GetExecOutputErrors, GetExecOutputResponses, GetExecResponses, GetFileStatData, GetFileStatErrors, GetFileStatResponses, GetTaskData, GetTaskErrors, GetTaskResponses, ListDirectoryData, ListDirectoryErrors, ListDirectoryResponses, ListExecsData, ListExecsErrors, ListExecsResponses, ListPortsData, ListPortsErrors, ListPortsResponses, ListSetupTasksData, ListSetupTasksErrors, ListSetupTasksResponses, ListTasksData, ListTasksErrors, ListTasksResponses, PerformFileActionData, PerformFileActionErrors, PerformFileActionResponses, ReadFileData, ReadFileErrors, ReadFileResponses, StreamExecsListData, StreamExecsListErrors, StreamExecsListResponses, StreamPortsListData, StreamPortsListErrors, StreamPortsListResponses, UpdateExecData, UpdateExecErrors, UpdateExecResponses } from './types.gen'; export type Options = Options2 & { /** @@ -442,3 +442,20 @@ export const streamPortsList = (options?: ...options }); }; + +/** + * Watch directory changes using Server-Sent Events (SSE) + * Watch a directory for file system changes and stream events via SSE. + */ +export const createWatcher = (options: Options) => { + return (options.client ?? client).sse.get({ + security: [ + { + scheme: 'bearer', + type: 'http' + } + ], + url: '/api/v1/stream/directories/watcher/{path}', + ...options + }); +}; diff --git a/src/api-clients/pint/types.gen.ts b/src/api-clients/pint/types.gen.ts index cc3faa0..8a0363c 100644 --- a/src/api-clients/pint/types.gen.ts +++ b/src/api-clients/pint/types.gen.ts @@ -123,6 +123,10 @@ export type ExecItem = { * Whether the exec is interactive */ interactive: boolean; + /** + * Whether the exec is using a pty + */ + pty: boolean; /** * Exit code of the process (only present when process has exited) */ @@ -153,6 +157,10 @@ export type CreateExecRequest = { * Whether to start interactive shell session or not (defaults to false) */ interactive?: boolean; + /** + * Whether to start pty shell session or not (defaults to false) + */ + pty?: boolean; }; export type UpdateExecRequest = { @@ -169,6 +177,29 @@ export type ExecDeleteResponse = { message: string; }; +export type ExecStdout = { + /** + * Type of the exec output + */ + type: 'stdout' | 'stderr'; + /** + * Data associated with the exec output + */ + output: string; + /** + * Sequence number of the output message + */ + sequence: number; + /** + * Timestamp of when the output was generated + */ + timestamp?: string; + /** + * Exit code of the process (only present when process has exited) + */ + exitCode?: number; +}; + export type ExecStdin = { /** * Type of the exec input @@ -298,29 +329,6 @@ export type PortsListResponse = { ports: Array; }; -export type ExecStdout = { - /** - * Type of the exec output - */ - type: 'stdout' | 'stderr'; - /** - * Data associated with the exec output - */ - output: string; - /** - * Sequence number of the output message - */ - sequence: number; - /** - * Timestamp of when the output was generated - */ - timestamp?: string; - /** - * Exit code of the process (only present when process has exited) - */ - exitCode?: number; -}; - export type Task = TaskItem; export type DeleteFileData = { @@ -1270,3 +1278,54 @@ export type StreamPortsListResponses = { }; export type StreamPortsListResponse = StreamPortsListResponses[keyof StreamPortsListResponses]; + +export type CreateWatcherData = { + body?: never; + path: { + /** + * Directory path to watch + */ + path: string; + }; + query?: { + /** + * Whether to watch directories recursively + */ + recursive?: boolean; + /** + * Glob patterns to ignore certain files or directories (can be specified multiple times) + */ + ignorePatterns?: Array; + }; + url: '/api/v1/stream/directories/watcher/{path}'; +}; + +export type CreateWatcherErrors = { + /** + * Bad Request - Path is required or invalid path + */ + 400: _Error; + /** + * Unauthorized + */ + 401: _Error; + /** + * Internal Server Error - Failed to create file + */ + 500: _Error; + /** + * Unexpected Error + */ + default: _Error; +}; + +export type CreateWatcherError = CreateWatcherErrors[keyof CreateWatcherErrors]; + +export type CreateWatcherResponses = { + /** + * Server-Sent Events stream of directory files updates + */ + 200: string; +}; + +export type CreateWatcherResponse = CreateWatcherResponses[keyof CreateWatcherResponses]; diff --git a/src/types.ts b/src/types.ts index 952d14e..09ecc3f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -13,6 +13,9 @@ export interface PitcherManagerResponse { latestPitcherVersion: string; pitcherToken: string; cluster: string; + vmAgentType: string; + pintURL?: string; + pintToken?: string; } export interface SystemMetricsStatus { diff --git a/tests/e2e/helpers.ts b/tests/e2e/helpers.ts index 17c9a87..c5af65b 100644 --- a/tests/e2e/helpers.ts +++ b/tests/e2e/helpers.ts @@ -1,19 +1,22 @@ -import { CodeSandbox } from '../../src/index.js'; +import { CodeSandbox } from "../../src/index.js"; /** * Test template ID used across e2e tests */ -export const TEST_TEMPLATE_ID = process.env.CSB_TEST_TEMPLATE_ID ?? ''; +export const TEST_TEMPLATE_ID = + process.env.CSB_TEST_TEMPLATE_ID ?? "pt_FXCz5KGvDQsafzZz7awrSe"; /** * Initialize SDK with API key from environment */ export function initializeSDK(): CodeSandbox { - const apiKey = process.env.CSB_API_KEY; - if (!apiKey) { - throw new Error('CSB_API_KEY environment variable is required for e2e tests'); + if (process.env.CSB_BASE_URL) { + return new CodeSandbox(process.env.CSB_API_KEY, { + baseUrl: process.env.CSB_BASE_URL, + }); } - return new CodeSandbox(apiKey); + + return new CodeSandbox(process.env.CSB_API_KEY); } /** diff --git a/tests/e2e/sandbox-terminals.test.ts b/tests/e2e/sandbox-terminals.test.ts index 6400417..92b6354 100644 --- a/tests/e2e/sandbox-terminals.test.ts +++ b/tests/e2e/sandbox-terminals.test.ts @@ -1,10 +1,10 @@ -import { describe, it, expect, beforeAll, afterAll } from 'vitest'; -import { CodeSandbox } from '../../src/index.js'; -import { Sandbox } from '../../src/Sandbox.js'; -import { SandboxClient } from '../../src/SandboxClient/index.js'; -import { initializeSDK, TEST_TEMPLATE_ID } from './helpers.js'; +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { CodeSandbox } from "../../src/index.js"; +import { Sandbox } from "../../src/Sandbox.js"; +import { SandboxClient } from "../../src/SandboxClient/index.js"; +import { initializeSDK, TEST_TEMPLATE_ID } from "./helpers.js"; -describe('Sandbox Terminals', () => { +describe("Sandbox Terminals", () => { let sdk: CodeSandbox; let sandbox: Sandbox | undefined; let client: SandboxClient | undefined; @@ -31,7 +31,7 @@ describe('Sandbox Terminals', () => { client = undefined; } } catch (error) { - console.error('Failed to dispose client:', error); + console.error("Failed to dispose client:", error); } if (sandboxId) { @@ -39,19 +39,24 @@ describe('Sandbox Terminals', () => { await sdk.sandboxes.shutdown(sandboxId); await sdk.sandboxes.delete(sandboxId); } catch (error) { - console.error('Failed to cleanup test sandbox:', sandboxId, error); + console.error("Failed to cleanup test sandbox:", sandboxId, error); try { await sdk.sandboxes.delete(sandboxId); } catch (deleteError) { - console.error('Failed to force delete sandbox:', sandboxId, deleteError); + console.error( + "Failed to force delete sandbox:", + sandboxId, + deleteError + ); } } } }); - describe('Terminal creation', () => { - it('should create a terminal', async () => { - if (!client || !sandbox) throw new Error('Client or sandbox not initialized'); + describe("Terminal creation", () => { + it("should create a terminal", async () => { + if (!client || !sandbox) + throw new Error("Client or sandbox not initialized"); const terminal = await client.terminals.create(); expect(terminal).toBeDefined(); @@ -61,10 +66,11 @@ describe('Sandbox Terminals', () => { await terminal.kill(); }); - it('should create terminal with custom dimensions', async () => { - if (!client || !sandbox) throw new Error('Client or sandbox not initialized'); + it("should create terminal with custom dimensions", async () => { + if (!client || !sandbox) + throw new Error("Client or sandbox not initialized"); - const terminal = await client.terminals.create('bash', { + const terminal = await client.terminals.create("bash", { dimensions: { cols: 120, rows: 40 }, }); expect(terminal).toBeDefined(); @@ -75,9 +81,10 @@ describe('Sandbox Terminals', () => { }); }); - describe('Terminal listing', () => { - it('should get all terminals', async () => { - if (!client || !sandbox) throw new Error('Client or sandbox not initialized'); + describe("Terminal listing", () => { + it("should get all terminals", async () => { + if (!client || !sandbox) + throw new Error("Client or sandbox not initialized"); const terminal1 = await client.terminals.create(); const terminal2 = await client.terminals.create(); @@ -91,8 +98,9 @@ describe('Sandbox Terminals', () => { await terminal2.kill(); }, 15000); - it('should get terminal by ID', async () => { - if (!client || !sandbox) throw new Error('Client or sandbox not initialized'); + it("should get terminal by ID", async () => { + if (!client || !sandbox) + throw new Error("Client or sandbox not initialized"); const terminal = await client.terminals.create(); const retrieved = await client.terminals.get(terminal.id); @@ -107,9 +115,10 @@ describe('Sandbox Terminals', () => { }); }); - describe('Terminal operations', () => { - it('should write to terminal', async () => { - if (!client || !sandbox) throw new Error('Client or sandbox not initialized'); + describe("Terminal operations", () => { + it("should write to terminal", async () => { + if (!client || !sandbox) + throw new Error("Client or sandbox not initialized"); const terminal = await client.terminals.create(); @@ -123,8 +132,9 @@ describe('Sandbox Terminals', () => { await terminal.kill(); }); - it('should run command in terminal', async () => { - if (!client || !sandbox) throw new Error('Client or sandbox not initialized'); + it("should run command in terminal", async () => { + if (!client || !sandbox) + throw new Error("Client or sandbox not initialized"); const terminal = await client.terminals.create(); @@ -138,19 +148,23 @@ describe('Sandbox Terminals', () => { await terminal.kill(); }); - it('should receive output from terminal', async () => { - if (!client || !sandbox) throw new Error('Client or sandbox not initialized'); + it("should receive output from terminal", async () => { + if (!client || !sandbox) + throw new Error("Client or sandbox not initialized"); const terminal = await client.terminals.create(); let receivedOutput = false; // Listen for output const disposable = terminal.onOutput((data) => { - if (data.includes('unique_test_string')) { + if (data.includes("unique_test_string")) { receivedOutput = true; } }); + // Users have to open first to get current output + await terminal.open(); + // Write a command await terminal.write('echo "unique_test_string"\n'); @@ -165,9 +179,10 @@ describe('Sandbox Terminals', () => { }, 10000); }); - describe('Terminal lifecycle', () => { - it('should kill terminal', async () => { - if (!client || !sandbox) throw new Error('Client or sandbox not initialized'); + describe("Terminal lifecycle", () => { + it("should kill terminal", async () => { + if (!client || !sandbox) + throw new Error("Client or sandbox not initialized"); const terminal = await client.terminals.create(); expect(terminal).toBeDefined(); @@ -179,8 +194,9 @@ describe('Sandbox Terminals', () => { expect(terminal.id).toBeTruthy(); }); - it('should handle multiple terminals', async () => { - if (!client || !sandbox) throw new Error('Client or sandbox not initialized'); + it("should handle multiple terminals", async () => { + if (!client || !sandbox) + throw new Error("Client or sandbox not initialized"); const terminals = await Promise.all([ client.terminals.create(), diff --git a/tests/emitter-subscription.test.ts b/tests/emitter-subscription.test.ts index 669d226..8c31b33 100644 --- a/tests/emitter-subscription.test.ts +++ b/tests/emitter-subscription.test.ts @@ -1,211 +1,218 @@ -import { describe, it, expect, vi } from 'vitest' -import { EmitterSubscription } from '../src/utils/event' -import { Disposable } from '../src/utils/disposable' +import { describe, it, expect, vi } from "vitest"; +import { EmitterSubscription } from "../src/utils/event"; +import { Disposable } from "../src/utils/disposable"; +import { sleep } from "../src/utils/sleep"; -describe('EmitterSubscription', () => { - it('should create subscription when first listener is added', () => { - const createSubscription = vi.fn(() => Disposable.create(() => {})) - const subscription = new EmitterSubscription(createSubscription) +describe("EmitterSubscription", () => { + it("should create subscription when first listener is added", () => { + const createSubscription = vi.fn(() => Disposable.create(() => {})); + const subscription = new EmitterSubscription(createSubscription); - expect(createSubscription).not.toHaveBeenCalled() + expect(createSubscription).not.toHaveBeenCalled(); - const disposable = subscription.event(() => {}) + const disposable = subscription.event(() => {}); - expect(createSubscription).toHaveBeenCalledTimes(1) + expect(createSubscription).toHaveBeenCalledTimes(1); - disposable.dispose() - }) + disposable.dispose(); + }); - it('should not create multiple subscriptions for multiple listeners', () => { - const createSubscription = vi.fn(() => Disposable.create(() => {})) - const subscription = new EmitterSubscription(createSubscription) + it("should not create multiple subscriptions for multiple listeners", () => { + const createSubscription = vi.fn(() => Disposable.create(() => {})); + const subscription = new EmitterSubscription(createSubscription); - const disposable1 = subscription.event(() => {}) - const disposable2 = subscription.event(() => {}) - const disposable3 = subscription.event(() => {}) + const disposable1 = subscription.event(() => {}); + const disposable2 = subscription.event(() => {}); + const disposable3 = subscription.event(() => {}); - expect(createSubscription).toHaveBeenCalledTimes(1) + expect(createSubscription).toHaveBeenCalledTimes(1); - disposable1.dispose() - disposable2.dispose() - disposable3.dispose() - }) + disposable1.dispose(); + disposable2.dispose(); + disposable3.dispose(); + }); - it('should fire events to all listeners', () => { + it("should fire events to all listeners", async () => { const subscription = new EmitterSubscription((fire) => { - fire(42) - return Disposable.create(() => {}) - }) + setTimeout(() => { + fire(42); + }, 10); + return Disposable.create(() => {}); + }); - const listener1 = vi.fn() - const listener2 = vi.fn() - const listener3 = vi.fn() + const listener1 = vi.fn(); + const listener2 = vi.fn(); + const listener3 = vi.fn(); - subscription.event(listener1) - subscription.event(listener2) - subscription.event(listener3) + subscription.event(listener1); + subscription.event(listener2); + subscription.event(listener3); - expect(listener1).toHaveBeenCalledWith(42) - expect(listener2).toHaveBeenCalledWith(42) - expect(listener3).toHaveBeenCalledWith(42) - }) + await sleep(100); - it('should allow firing events from subscription callback', () => { - let fireFn: ((value: number) => void) | undefined + expect(listener1).toHaveBeenCalledWith(42); + expect(listener2).toHaveBeenCalledWith(42); + expect(listener3).toHaveBeenCalledWith(42); + }); + + it("should allow firing events from subscription callback", () => { + let fireFn: ((value: number) => void) | undefined; const subscription = new EmitterSubscription((fire) => { - fireFn = fire - return Disposable.create(() => {}) - }) + fireFn = fire; + return Disposable.create(() => {}); + }); - const listener = vi.fn() - subscription.event(listener) + const listener = vi.fn(); + subscription.event(listener); - expect(fireFn).toBeDefined() + expect(fireFn).toBeDefined(); - fireFn!(100) - fireFn!(200) - fireFn!(300) + fireFn!(100); + fireFn!(200); + fireFn!(300); - expect(listener).toHaveBeenCalledTimes(3) - expect(listener).toHaveBeenNthCalledWith(1, 100) - expect(listener).toHaveBeenNthCalledWith(2, 200) - expect(listener).toHaveBeenNthCalledWith(3, 300) - }) + expect(listener).toHaveBeenCalledTimes(3); + expect(listener).toHaveBeenNthCalledWith(1, 100); + expect(listener).toHaveBeenNthCalledWith(2, 200); + expect(listener).toHaveBeenNthCalledWith(3, 300); + }); - it('should dispose subscription when last listener is removed', () => { - const dispose = vi.fn() - const createSubscription = vi.fn(() => Disposable.create(dispose)) - const subscription = new EmitterSubscription(createSubscription) + it("should dispose subscription when last listener is removed", () => { + const dispose = vi.fn(); + const createSubscription = vi.fn(() => Disposable.create(dispose)); + const subscription = new EmitterSubscription(createSubscription); - const disposable1 = subscription.event(() => {}) - const disposable2 = subscription.event(() => {}) + const disposable1 = subscription.event(() => {}); + const disposable2 = subscription.event(() => {}); - expect(dispose).not.toHaveBeenCalled() + expect(dispose).not.toHaveBeenCalled(); - disposable1.dispose() - expect(dispose).not.toHaveBeenCalled() + disposable1.dispose(); + expect(dispose).not.toHaveBeenCalled(); - disposable2.dispose() - expect(dispose).toHaveBeenCalledTimes(1) - }) + disposable2.dispose(); + expect(dispose).toHaveBeenCalledTimes(1); + }); - it('should recreate subscription if listener is added again after all removed', () => { - const dispose = vi.fn() - const createSubscription = vi.fn(() => Disposable.create(dispose)) - const subscription = new EmitterSubscription(createSubscription) + it("should recreate subscription if listener is added again after all removed", () => { + const dispose = vi.fn(); + const createSubscription = vi.fn(() => Disposable.create(dispose)); + const subscription = new EmitterSubscription(createSubscription); - const disposable1 = subscription.event(() => {}) - disposable1.dispose() + const disposable1 = subscription.event(() => {}); + disposable1.dispose(); - expect(createSubscription).toHaveBeenCalledTimes(1) - expect(dispose).toHaveBeenCalledTimes(1) + expect(createSubscription).toHaveBeenCalledTimes(1); + expect(dispose).toHaveBeenCalledTimes(1); - const disposable2 = subscription.event(() => {}) + const disposable2 = subscription.event(() => {}); - expect(createSubscription).toHaveBeenCalledTimes(2) - expect(dispose).toHaveBeenCalledTimes(1) + expect(createSubscription).toHaveBeenCalledTimes(2); + expect(dispose).toHaveBeenCalledTimes(1); - disposable2.dispose() - expect(dispose).toHaveBeenCalledTimes(2) - }) + disposable2.dispose(); + expect(dispose).toHaveBeenCalledTimes(2); + }); - it('should stop firing to disposed listeners', () => { - let fireFn: ((value: number) => void) | undefined + it("should stop firing to disposed listeners", () => { + let fireFn: ((value: number) => void) | undefined; const subscription = new EmitterSubscription((fire) => { - fireFn = fire - return Disposable.create(() => {}) - }) - - const listener1 = vi.fn() - const listener2 = vi.fn() - const listener3 = vi.fn() - - const disposable1 = subscription.event(listener1) - subscription.event(listener2) - subscription.event(listener3) - - fireFn!(1) - expect(listener1).toHaveBeenCalledTimes(1) - expect(listener2).toHaveBeenCalledTimes(1) - expect(listener3).toHaveBeenCalledTimes(1) - - disposable1.dispose() - - fireFn!(2) - expect(listener1).toHaveBeenCalledTimes(1) // Not called again - expect(listener2).toHaveBeenCalledTimes(2) - expect(listener3).toHaveBeenCalledTimes(2) - }) - - it('should cleanup everything on dispose', () => { - const subscriptionDispose = vi.fn() - const createSubscription = vi.fn(() => Disposable.create(subscriptionDispose)) - - let fireFn: ((value: number) => void) | undefined + fireFn = fire; + return Disposable.create(() => {}); + }); + + const listener1 = vi.fn(); + const listener2 = vi.fn(); + const listener3 = vi.fn(); + + const disposable1 = subscription.event(listener1); + subscription.event(listener2); + subscription.event(listener3); + + fireFn!(1); + expect(listener1).toHaveBeenCalledTimes(1); + expect(listener2).toHaveBeenCalledTimes(1); + expect(listener3).toHaveBeenCalledTimes(1); + + disposable1.dispose(); + + fireFn!(2); + expect(listener1).toHaveBeenCalledTimes(1); // Not called again + expect(listener2).toHaveBeenCalledTimes(2); + expect(listener3).toHaveBeenCalledTimes(2); + }); + + it("should cleanup everything on dispose", () => { + const subscriptionDispose = vi.fn(); + const createSubscription = vi.fn(() => + Disposable.create(subscriptionDispose) + ); + + let fireFn: ((value: number) => void) | undefined; const subscription = new EmitterSubscription((fire) => { - fireFn = fire - return Disposable.create(subscriptionDispose) - }) + fireFn = fire; + return Disposable.create(subscriptionDispose); + }); - const listener = vi.fn() - subscription.event(listener) + const listener = vi.fn(); + subscription.event(listener); - subscription.dispose() + subscription.dispose(); - expect(subscriptionDispose).toHaveBeenCalledTimes(1) + expect(subscriptionDispose).toHaveBeenCalledTimes(1); // Should not fire to listeners after dispose - fireFn!(42) - expect(listener).not.toHaveBeenCalled() - }) + fireFn!(42); + expect(listener).not.toHaveBeenCalled(); + }); - it('should work with interval example', () => { - vi.useFakeTimers() + it("should work with interval example", () => { + vi.useFakeTimers(); - let intervalId: NodeJS.Timeout + let intervalId: NodeJS.Timeout; const subscription = new EmitterSubscription((fire) => { - intervalId = setInterval(() => fire(Date.now()), 1000) - return Disposable.create(() => clearInterval(intervalId)) - }) + intervalId = setInterval(() => fire(Date.now()), 1000); + return Disposable.create(() => clearInterval(intervalId)); + }); - const listener = vi.fn() - const disposable = subscription.event(listener) + const listener = vi.fn(); + const disposable = subscription.event(listener); - vi.advanceTimersByTime(3500) - expect(listener).toHaveBeenCalledTimes(3) + vi.advanceTimersByTime(3500); + expect(listener).toHaveBeenCalledTimes(3); - disposable.dispose() + disposable.dispose(); // Should not receive more events after dispose - vi.advanceTimersByTime(5000) - expect(listener).toHaveBeenCalledTimes(3) + vi.advanceTimersByTime(5000); + expect(listener).toHaveBeenCalledTimes(3); - vi.useRealTimers() - }) + vi.useRealTimers(); + }); - it('should handle multiple add/remove cycles correctly', () => { - const dispose = vi.fn() - const createSubscription = vi.fn(() => Disposable.create(dispose)) - const subscription = new EmitterSubscription(createSubscription) + it("should handle multiple add/remove cycles correctly", () => { + const dispose = vi.fn(); + const createSubscription = vi.fn(() => Disposable.create(dispose)); + const subscription = new EmitterSubscription(createSubscription); // Cycle 1 - const d1 = subscription.event(() => {}) - d1.dispose() - expect(createSubscription).toHaveBeenCalledTimes(1) - expect(dispose).toHaveBeenCalledTimes(1) + const d1 = subscription.event(() => {}); + d1.dispose(); + expect(createSubscription).toHaveBeenCalledTimes(1); + expect(dispose).toHaveBeenCalledTimes(1); // Cycle 2 - const d2 = subscription.event(() => {}) - d2.dispose() - expect(createSubscription).toHaveBeenCalledTimes(2) - expect(dispose).toHaveBeenCalledTimes(2) + const d2 = subscription.event(() => {}); + d2.dispose(); + expect(createSubscription).toHaveBeenCalledTimes(2); + expect(dispose).toHaveBeenCalledTimes(2); // Cycle 3 - const d3 = subscription.event(() => {}) - d3.dispose() - expect(createSubscription).toHaveBeenCalledTimes(3) - expect(dispose).toHaveBeenCalledTimes(3) - }) -}) \ No newline at end of file + const d3 = subscription.event(() => {}); + d3.dispose(); + expect(createSubscription).toHaveBeenCalledTimes(3); + expect(dispose).toHaveBeenCalledTimes(3); + }); +}); diff --git a/tests/pint-fs-watcher.test.ts b/tests/pint-fs-watcher.test.ts new file mode 100644 index 0000000..5acf273 --- /dev/null +++ b/tests/pint-fs-watcher.test.ts @@ -0,0 +1,268 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { PintFsClient } from '../src/PintClient/fs' +import { Client } from '../src/api-clients/pint/client' +import * as pintApi from '../src/api-clients/pint' + +// Mock the pint API functions +vi.mock('../src/api-clients/pint', () => ({ + createWatcher: vi.fn(), + createFile: vi.fn(), + readFile: vi.fn(), + listDirectory: vi.fn(), + deleteDirectory: vi.fn(), + createDirectory: vi.fn(), + getFileStat: vi.fn(), + performFileAction: vi.fn(), +})) + +describe('PintFsClient filesystem watcher', () => { + let fsClient: PintFsClient + let mockApiClient: Client + let mockCreateWatcher: any + + beforeEach(() => { + // Create a mock API client + mockApiClient = {} as Client + + // Create instance of PintFsClient + fsClient = new PintFsClient(mockApiClient) + + // Get reference to mocked functions + mockCreateWatcher = vi.mocked(pintApi.createWatcher) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it('should successfully start watching a directory', async () => { + const path = '/test/directory' + const options = { recursive: true, excludes: ['*.log', 'node_modules/*'] } + const onEvent = vi.fn() + + // Mock the stream generator + async function* mockStream() { + yield 'data: {"paths": ["/test/directory/file1.txt"], "type": "add"}' + yield 'data: {"paths": ["/test/directory/file2.txt"], "type": "change"}' + } + + // Mock createWatcher to return a stream + mockCreateWatcher.mockResolvedValue({ + stream: mockStream() + }) + + // Call watch method + const result = await fsClient.watch(path, options, onEvent) + + // Verify the result + expect(result.type).toBe('success') + expect(result).toHaveProperty('dispose') + + // Verify createWatcher was called with correct parameters + expect(mockCreateWatcher).toHaveBeenCalledWith({ + client: mockApiClient, + path: { path }, + query: { + recursive: true, + ignorePatterns: ['*.log', 'node_modules/*'] + }, + signal: expect.any(AbortSignal) + }) + + // Wait a bit for the async stream processing + await new Promise(resolve => setTimeout(resolve, 100)) + + // Verify events were parsed and fired + expect(onEvent).toHaveBeenCalledTimes(2) + expect(onEvent).toHaveBeenCalledWith({ + paths: ['/test/directory/file1.txt'], + type: 'add' + }) + expect(onEvent).toHaveBeenCalledWith({ + paths: ['/test/directory/file2.txt'], + type: 'change' + }) + }) + + it('should handle watcher with minimal options', async () => { + const path = '/simple/path' + const options = {} + const onEvent = vi.fn() + + // Mock empty stream + async function* mockStream() { + // Empty stream + } + + mockCreateWatcher.mockResolvedValue({ + stream: mockStream() + }) + + const result = await fsClient.watch(path, options, onEvent) + + expect(result.type).toBe('success') + expect(mockCreateWatcher).toHaveBeenCalledWith({ + client: mockApiClient, + path: { path }, + query: { + recursive: undefined, + ignorePatterns: undefined + }, + signal: expect.any(AbortSignal) + }) + }) + + it('should handle filesystem events correctly', async () => { + const path = '/test/path' + const options = { recursive: false } + const onEvent = vi.fn() + + // Mock stream with different event types + async function* mockStream() { + yield 'data: {"paths": ["/test/path/new-file.txt"], "type": "add"}' + yield 'data: {"paths": ["/test/path/modified-file.txt"], "type": "change"}' + yield 'data: {"paths": ["/test/path/deleted-file.txt"], "type": "remove"}' + } + + mockCreateWatcher.mockResolvedValue({ + stream: mockStream() + }) + + const result = await fsClient.watch(path, options, onEvent) + expect(result.type).toBe('success') + + // Wait for stream processing + await new Promise(resolve => setTimeout(resolve, 100)) + + // Verify all event types were handled + expect(onEvent).toHaveBeenCalledTimes(3) + expect(onEvent).toHaveBeenNthCalledWith(1, { + paths: ['/test/path/new-file.txt'], + type: 'add' + }) + expect(onEvent).toHaveBeenNthCalledWith(2, { + paths: ['/test/path/modified-file.txt'], + type: 'change' + }) + expect(onEvent).toHaveBeenNthCalledWith(3, { + paths: ['/test/path/deleted-file.txt'], + type: 'remove' + }) + }) + + it('should handle malformed stream events gracefully', async () => { + const path = '/test/path' + const options = {} + const onEvent = vi.fn() + + // Mock stream with malformed data + async function* mockStream() { + yield 'data: {"paths": ["/test/path/good-file.txt"], "type": "add"}' + yield 'data: invalid json' + yield 'data: {"paths": ["/test/path/another-good-file.txt"], "type": "change"}' + } + + mockCreateWatcher.mockResolvedValue({ + stream: mockStream() + }) + + // Spy on console.warn to verify error handling + const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) + + const result = await fsClient.watch(path, options, onEvent) + expect(result.type).toBe('success') + + // Wait for stream processing + await new Promise(resolve => setTimeout(resolve, 100)) + + // Verify only valid events were processed + expect(onEvent).toHaveBeenCalledTimes(2) + expect(onEvent).toHaveBeenNthCalledWith(1, { + paths: ['/test/path/good-file.txt'], + type: 'add' + }) + expect(onEvent).toHaveBeenNthCalledWith(2, { + paths: ['/test/path/another-good-file.txt'], + type: 'change' + }) + + // Verify warning was logged for malformed data + expect(consoleSpy).toHaveBeenCalledWith( + 'Failed to parse filesystem watch event:', + expect.any(Error) + ) + + consoleSpy.mockRestore() + }) + + it('should allow disposal of watcher', async () => { + const path = '/test/path' + const options = {} + const onEvent = vi.fn() + + // Mock stream that would run indefinitely + async function* mockStream() { + let count = 0 + while (true) { + yield `data: {"paths": ["/test/path/file${count}.txt"], "type": "add"}` + count++ + // Add a small delay to prevent tight loop + await new Promise(resolve => setTimeout(resolve, 10)) + } + } + + mockCreateWatcher.mockResolvedValue({ + stream: mockStream() + }) + + const result = await fsClient.watch(path, options, onEvent) + expect(result.type).toBe('success') + + if (result.type === 'success') { + expect(typeof result.dispose).toBe('function') + + // Let it run for a bit + await new Promise(resolve => setTimeout(resolve, 50)) + + // Dispose the watcher + result.dispose() + + // The dispose function should abort the controller + expect(() => result.dispose()).not.toThrow() + } + }) + + it('should handle createWatcher promise rejection', async () => { + const path = '/test/path' + const options = {} + const onEvent = vi.fn() + + // Mock createWatcher to reject + mockCreateWatcher.mockRejectedValue(new Error('Network error')) + + const result = await fsClient.watch(path, options, onEvent) + + expect(result.type).toBe('error') + if (result.type === 'error') { + expect(result.error).toBe('Network error') + expect(result.errno).toBe(null) + } + }) + + it('should handle unknown errors', async () => { + const path = '/test/path' + const options = {} + const onEvent = vi.fn() + + // Mock createWatcher to reject with non-Error + mockCreateWatcher.mockRejectedValue('String error') + + const result = await fsClient.watch(path, options, onEvent) + + expect(result.type).toBe('error') + if (result.type === 'error') { + expect(result.error).toBe('Unknown error') + expect(result.errno).toBe(null) + } + }) +}) \ No newline at end of file diff --git a/tests/pint-shells-client.test.ts b/tests/pint-shells-client.test.ts index a03ee1d..347ba2a 100644 --- a/tests/pint-shells-client.test.ts +++ b/tests/pint-shells-client.test.ts @@ -1,13 +1,12 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { PintShellsClient } from '../src/PintClient/execs'; -import { Client } from '../src/api-clients/pint/client'; -import * as pintApi from '../src/api-clients/pint'; -import { ExecItem } from '../src/api-clients/pint'; -import { ShellSize, ShellProcessType } from '../src/pitcher-protocol/messages/shell'; -import { IDisposable } from '../src/utils/disposable'; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { PintShellsClient } from "../src/PintClient/execs"; +import { Client } from "../src/api-clients/pint/client"; +import * as pintApi from "../src/api-clients/pint"; +import { ExecItem } from "../src/api-clients/pint"; +import { IDisposable } from "../src/utils/disposable"; // Mock the API functions -vi.mock('../src/api-clients/pint', () => ({ +vi.mock("../src/api-clients/pint", () => ({ createExec: vi.fn(), getExec: vi.fn(), listExecs: vi.fn(), @@ -19,9 +18,9 @@ vi.mock('../src/api-clients/pint', () => ({ })); // Mock the utils parseStreamEvent function -vi.mock('../src/PintClient/utils', () => ({ +vi.mock("../src/PintClient/utils", () => ({ parseStreamEvent: vi.fn((evt) => { - if (typeof evt === 'string') { + if (typeof evt === "string") { return JSON.parse(evt.substring(5)); } return evt; @@ -38,134 +37,132 @@ const createMockResponse = (data: any, error?: any) => ({ // Mock ExecItem for testing const createMockExecItem = (overrides: Partial = {}): ExecItem => ({ - id: 'exec-123', - command: 'bash', + id: "exec-123", + command: "bash", args: [], interactive: true, - status: 'RUNNING', + status: "RUNNING", exitCode: 0, pid: 1234, ...overrides, }); -describe('PintShellsClient', () => { +describe("PintShellsClient", () => { let client: PintShellsClient; let mockApiClient: Client; beforeEach(() => { vi.clearAllMocks(); mockApiClient = {} as Client; - client = new PintShellsClient(mockApiClient, 'sandbox-123'); + client = new PintShellsClient(mockApiClient, "sandbox-123"); }); - describe('create', () => { - it('should successfully create a new shell with command', async () => { + describe("create", () => { + it("should successfully create a new shell with command", async () => { const mockExec = createMockExecItem(); const mockResponse = createMockResponse(mockExec); - + vi.mocked(pintApi.createExec).mockResolvedValue(mockResponse); - - // Mock the open method call - const mockOpenResponse = createMockResponse(mockExec); - vi.mocked(pintApi.getExec).mockResolvedValue(mockOpenResponse); - vi.mocked(pintApi.getExecOutput).mockResolvedValue({ - ...createMockResponse({}), - stream: (async function* (): AsyncGenerator { - yield 'data:{"type":"stdout","output":"Welcome","sequence":1,"timestamp":"2023-01-01T12:00:00Z"}'; - })(), - }); - const result = await client.create( - '/workspace', - { cols: 80, rows: 24 }, - 'npm start', - 'COMMAND', - false - ); + const result = await client.create({ + command: "npm", + args: ["start"], + projectPath: "/workspace", + size: { cols: 80, rows: 24 }, + type: "COMMAND", + }); expect(result).toEqual({ isSystemShell: true, name: JSON.stringify({ - type: 'command', - command: 'bash', - name: '', + type: "command", + command: "bash", + name: "", }), - ownerUsername: 'root', - shellId: 'exec-123', - shellType: 'TERMINAL', - startCommand: 'bash', - status: 'RUNNING', + ownerUsername: "root", + shellId: "exec-123", + shellType: "TERMINAL", + startCommand: "bash", + status: "RUNNING", buffer: [], }); expect(pintApi.createExec).toHaveBeenCalledWith({ client: mockApiClient, body: { - args: ['start'], - command: 'npm', + args: ["start"], + command: "npm", interactive: false, }, }); }); - it('should create a shell with default bash command', async () => { - const mockExec = createMockExecItem({ command: 'bash' }); + it("should create a shell with default bash command", async () => { + const mockExec = createMockExecItem({ command: "bash" }); const mockResponse = createMockResponse(mockExec); - + vi.mocked(pintApi.createExec).mockResolvedValue(mockResponse); - vi.mocked(pintApi.getExec).mockResolvedValue(mockResponse); - vi.mocked(pintApi.getExecOutput).mockResolvedValue({ - ...createMockResponse({}), - stream: (async function* (): AsyncGenerator {})(), - }); - await client.create('/workspace', { cols: 80, rows: 24 }); + await client.create({ + command: "bash", + args: [], + projectPath: "/workspace", + size: { cols: 80, rows: 24 }, + }); expect(pintApi.createExec).toHaveBeenCalledWith({ client: mockApiClient, body: { args: [], - command: 'bash', + command: "bash", interactive: true, }, }); }); - it('should handle API error during creation', async () => { - const mockResponse = createMockResponse(null, { message: 'Creation failed' }); + it("should handle API error during creation", async () => { + const mockResponse = createMockResponse(null, { + message: "Creation failed", + }); vi.mocked(pintApi.createExec).mockResolvedValue(mockResponse); await expect( - client.create('/workspace', { cols: 80, rows: 24 }) - ).rejects.toThrow('Creation failed'); + client.create({ + command: "bash", + args: [], + projectPath: "/workspace", + size: { cols: 80, rows: 24 }, + }) + ).rejects.toThrow("Creation failed"); }); - it('should set interactive based on shell type', async () => { + it("should set interactive based on shell type", async () => { const mockExec = createMockExecItem(); const mockResponse = createMockResponse(mockExec); - + vi.mocked(pintApi.createExec).mockResolvedValue(mockResponse); - vi.mocked(pintApi.getExec).mockResolvedValue(mockResponse); - vi.mocked(pintApi.getExecOutput).mockResolvedValue({ - ...createMockResponse({}), - stream: (async function* (): AsyncGenerator {})(), - }); - await client.create('/workspace', { cols: 80, rows: 24 }, 'echo test', 'TERMINAL'); + await client.create({ + command: "echo", + args: ["test"], + projectPath: "/workspace", + size: { cols: 80, rows: 24 }, + type: "TERMINAL", + }); expect(pintApi.createExec).toHaveBeenCalledWith({ client: mockApiClient, body: { - args: ['test'], - command: 'echo', + args: ["test"], + command: "echo", interactive: true, }, }); }); }); - describe('delete', () => { - it('should successfully delete an existing shell', async () => { + describe("delete", () => { + it("should successfully delete an existing shell", async () => { const mockExec = createMockExecItem(); const getResponse = createMockResponse(mockExec); const deleteResponse = createMockResponse({ success: true }); @@ -173,43 +170,43 @@ describe('PintShellsClient', () => { vi.mocked(pintApi.getExec).mockResolvedValue(getResponse); vi.mocked(pintApi.deleteExec).mockResolvedValue(deleteResponse); - const result = await client.delete('exec-123'); + const result = await client.delete("exec-123"); expect(result).toEqual({ isSystemShell: true, name: JSON.stringify({ - type: 'command', - command: 'bash', - name: '', + type: "command", + command: "bash", + name: "", }), - ownerUsername: 'root', - shellId: 'exec-123', - shellType: 'TERMINAL', - startCommand: 'bash', - status: 'RUNNING', + ownerUsername: "root", + shellId: "exec-123", + shellType: "TERMINAL", + startCommand: "bash", + status: "RUNNING", }); expect(pintApi.getExec).toHaveBeenCalledWith({ client: mockApiClient, - path: { id: 'exec-123' }, + path: { id: "exec-123" }, }); expect(pintApi.deleteExec).toHaveBeenCalledWith({ client: mockApiClient, - path: { id: 'exec-123' }, + path: { id: "exec-123" }, }); }); - it('should return null if shell does not exist', async () => { + it("should return null if shell does not exist", async () => { const getResponse = createMockResponse(null); vi.mocked(pintApi.getExec).mockResolvedValue(getResponse); - const result = await client.delete('nonexistent'); + const result = await client.delete("nonexistent"); expect(result).toBeNull(); expect(pintApi.deleteExec).not.toHaveBeenCalled(); }); - it('should return null if deletion fails', async () => { + it("should return null if deletion fails", async () => { const mockExec = createMockExecItem(); const getResponse = createMockResponse(mockExec); const deleteResponse = createMockResponse(null); @@ -217,25 +214,29 @@ describe('PintShellsClient', () => { vi.mocked(pintApi.getExec).mockResolvedValue(getResponse); vi.mocked(pintApi.deleteExec).mockResolvedValue(deleteResponse); - const result = await client.delete('exec-123'); + const result = await client.delete("exec-123"); expect(result).toBeNull(); }); - it('should handle exceptions gracefully', async () => { - vi.mocked(pintApi.getExec).mockRejectedValue(new Error('Network error')); + it("should handle exceptions gracefully", async () => { + vi.mocked(pintApi.getExec).mockRejectedValue(new Error("Network error")); - const result = await client.delete('exec-123'); + const result = await client.delete("exec-123"); expect(result).toBeNull(); }); }); - describe('getShells', () => { - it('should return list of shells converted from execs', async () => { + describe("getShells", () => { + it("should return list of shells converted from execs", async () => { const mockExecs = [ - createMockExecItem({ id: 'exec-1', command: 'bash' }), - createMockExecItem({ id: 'exec-2', command: 'npm', status: 'EXITED' as any }), + createMockExecItem({ id: "exec-1", command: "bash" }), + createMockExecItem({ + id: "exec-2", + command: "npm", + status: "EXITED" as any, + }), ]; const mockResponse = createMockResponse({ execs: mockExecs }); vi.mocked(pintApi.listExecs).mockResolvedValue(mockResponse); @@ -246,20 +247,20 @@ describe('PintShellsClient', () => { expect(result[0]).toEqual({ isSystemShell: true, name: JSON.stringify({ - type: 'command', - command: 'bash', - name: '', + type: "command", + command: "bash", + name: "", }), - ownerUsername: 'root', - shellId: 'exec-1', - shellType: 'TERMINAL', - startCommand: 'bash', - status: 'RUNNING', + ownerUsername: "root", + shellId: "exec-1", + shellType: "TERMINAL", + startCommand: "bash", + status: "RUNNING", }); - expect(result[1].status).toBe('EXITED'); + expect(result[1].status).toBe("EXITED"); }); - it('should return empty array if no execs found', async () => { + it("should return empty array if no execs found", async () => { const mockResponse = createMockResponse({ execs: [] }); vi.mocked(pintApi.listExecs).mockResolvedValue(mockResponse); @@ -268,7 +269,7 @@ describe('PintShellsClient', () => { expect(result).toEqual([]); }); - it('should handle API error by returning empty array', async () => { + it("should handle API error by returning empty array", async () => { const mockResponse = createMockResponse(null); vi.mocked(pintApi.listExecs).mockResolvedValue(mockResponse); @@ -278,117 +279,81 @@ describe('PintShellsClient', () => { }); }); - describe('open', () => { - it('should successfully open a shell and return with output buffer', async () => { - const mockExec = createMockExecItem(); - const getResponse = createMockResponse(mockExec); - const outputStream = (async function* (): AsyncGenerator { - yield 'data:{"type":"stdout","output":"Hello","sequence":1,"timestamp":"2023-01-01T12:00:00Z"}'; - })(); - - vi.mocked(pintApi.getExec).mockResolvedValue(getResponse); - vi.mocked(pintApi.getExecOutput).mockResolvedValue({ - ...createMockResponse({}), - stream: outputStream, - }); - - const result = await client.open('exec-123', { cols: 80, rows: 24 }); - expect(result).toEqual({ - buffer: ['Hello'], - isSystemShell: true, - name: JSON.stringify({ - type: 'command', - command: 'bash', - name: '', - }), - ownerUsername: 'root', - shellId: 'exec-123', - shellType: 'TERMINAL', - startCommand: 'bash', - status: 'RUNNING', - }); - - expect(pintApi.getExec).toHaveBeenCalledWith({ - client: mockApiClient, - path: { id: 'exec-123' }, - }); - }); - - it('should handle shell that does not exist', async () => { - const getResponse = createMockResponse(null, { message: 'Not found' }); - vi.mocked(pintApi.getExec).mockResolvedValue(getResponse); - - await expect( - client.open('nonexistent', { cols: 80, rows: 24 }) - ).rejects.toThrow('Not found'); - }); - }); - - describe('rename', () => { - it('should return null as rename is not implemented', async () => { - const result = await client.rename('exec-123', 'new-name'); + describe("rename", () => { + it("should return null as rename is not implemented", async () => { + const result = await client.rename("exec-123", "new-name"); expect(result).toBeNull(); }); }); - describe('restart', () => { - it('should successfully restart a shell', async () => { + describe("restart", () => { + it("should successfully restart a shell", async () => { const mockResponse = createMockResponse({ success: true }); vi.mocked(pintApi.updateExec).mockResolvedValue(mockResponse); - const result = await client.restart('exec-123'); + const result = await client.restart("exec-123"); expect(result).toBeNull(); expect(pintApi.updateExec).toHaveBeenCalledWith({ client: mockApiClient, - path: { id: 'exec-123' }, - body: { status: 'running' }, + path: { id: "exec-123" }, + body: { status: "running" }, }); }); - it('should handle restart failure gracefully', async () => { - vi.mocked(pintApi.updateExec).mockRejectedValue(new Error('Restart failed')); + it("should handle restart failure gracefully", async () => { + vi.mocked(pintApi.updateExec).mockRejectedValue( + new Error("Restart failed") + ); - const result = await client.restart('exec-123'); + const result = await client.restart("exec-123"); expect(result).toBeNull(); }); }); - describe('send', () => { - it('should successfully send input to shell', async () => { + describe("send", () => { + it("should successfully send input to shell", async () => { const mockResponse = createMockResponse({ success: true }); vi.mocked(pintApi.execExecStdin).mockResolvedValue(mockResponse); - const result = await client.send('exec-123', 'echo hello', { cols: 80, rows: 24 }); + const result = await client.send("exec-123", "echo hello", { + cols: 80, + rows: 24, + }); expect(result).toBeNull(); expect(pintApi.execExecStdin).toHaveBeenCalledWith({ client: mockApiClient, - path: { id: 'exec-123' }, + path: { id: "exec-123" }, body: { - type: 'stdin', - input: 'echo hello', + type: "stdin", + input: "echo hello", }, }); }); - it('should handle send failure gracefully', async () => { - vi.mocked(pintApi.execExecStdin).mockRejectedValue(new Error('Send failed')); + it("should handle send failure gracefully", async () => { + vi.mocked(pintApi.execExecStdin).mockRejectedValue( + new Error("Send failed") + ); - const result = await client.send('exec-123', 'test', { cols: 80, rows: 24 }); + const result = await client.send("exec-123", "test", { + cols: 80, + rows: 24, + }); expect(result).toBeNull(); }); }); - describe('convertExecToShellDTO', () => { - it('should convert ExecItem to ShellDTO format', async () => { + describe("convertExecToShellDTO", () => { + it("should convert ExecItem to ShellDTO format", async () => { const mockExec = createMockExecItem({ - id: 'test-exec', - command: 'node server.js', - status: 'RUNNING' as any, + id: "test-exec", + command: "node server.js", + status: "RUNNING" as any, }); // Access private method via bracket notation for testing @@ -397,58 +362,180 @@ describe('PintShellsClient', () => { expect(result).toEqual({ isSystemShell: true, name: JSON.stringify({ - type: 'command', - command: 'node server.js', - name: '', + type: "command", + command: "node server.js", + name: "", }), - ownerUsername: 'root', - shellId: 'test-exec', - shellType: 'TERMINAL', - startCommand: 'node server.js', - status: 'RUNNING', + ownerUsername: "root", + shellId: "test-exec", + shellType: "TERMINAL", + startCommand: "node server.js", + status: "RUNNING", }); }); }); - describe('event emitters', () => { - it('should have onShellExited event emitter', () => { - expect(client.onShellExited).toBeDefined(); - expect(typeof client.onShellExited).toBe('function'); - }); + describe("subscribe", () => { + it("should subscribe to shell exit events", async () => { + // Mock the stream for subscribeAndEvaluateExecsUpdates + const streamMock = (async function* (): AsyncGenerator< + string, + any, + unknown + > { + // First yield: initial state with RUNNING status + yield 'data:{"execs":[{"id":"exec-123","status":"RUNNING","exitCode":null,"command":"bash","args":[],"interactive":true,"pid":1234}]}'; + // Second yield: state change to EXITED + yield 'data:{"execs":[{"id":"exec-123","status":"EXITED","exitCode":0,"command":"bash","args":[],"interactive":true,"pid":1234}]}'; + })(); - it('should have onShellOut event emitter', () => { - expect(client.onShellOut).toBeDefined(); - expect(typeof client.onShellOut).toBe('function'); - }); + vi.mocked(pintApi.streamExecsList).mockResolvedValue({ + ...createMockResponse({}), + stream: streamMock, + }); + + const events: any[] = []; + const disposable = client.subscribe("exec-123", (event) => { + events.push(event); + }); + + // Give some time for the stream to process + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(events).toHaveLength(1); + expect(events[0]).toEqual({ + type: "exit", + exitCode: 0, + }); - it('should have onShellTerminated event emitter', () => { - expect(client.onShellTerminated).toBeDefined(); - expect(typeof client.onShellTerminated).toBe('function'); + disposable.dispose(); }); - it('should emit shell exit events when status changes from RUNNING to EXITED', async () => { - // Mock the stream for subscribeAndEvaluateExecsUpdates - const streamMock = (async function* (): AsyncGenerator { - yield 'data:{"execs":[{"id":"exec-123","status":"EXITED","exitCode":0,"command":"bash","args":[],"interactive":true,"pid":1234}]}'; - })(); - + it("should return disposable for cleanup", () => { + const streamMock = (async function* (): AsyncGenerator< + string, + any, + unknown + > {})(); + vi.mocked(pintApi.streamExecsList).mockResolvedValue({ ...createMockResponse({}), stream: streamMock, }); - // Test that the event emitter is properly set up - const unsubscribe: IDisposable = client.onShellExited((event) => { - expect(event.shellId).toBe('exec-123'); - expect(event.exitCode).toBe(0); + const disposable = client.subscribe("exec-123", () => {}); + + expect(disposable).toBeDefined(); + expect(typeof disposable.dispose).toBe("function"); + + disposable.dispose(); + }); + }); + + describe("subscribeOutput", () => { + it("should subscribe to shell output events", async () => { + const outputStream = (async function* (): AsyncGenerator< + string, + any, + unknown + > { + yield 'data:{"type":"stdout","output":"Hello World","sequence":1,"timestamp":"2023-01-01T12:00:00Z"}'; + yield 'data:{"type":"stdout","output":"Second line","sequence":2,"timestamp":"2023-01-01T12:00:01Z"}'; + })(); + + vi.mocked(pintApi.getExecOutput).mockResolvedValue({ + ...createMockResponse({}), + stream: outputStream, }); + const outputs: any[] = []; + const disposable = client.subscribeOutput( + "exec-123", + { cols: 80, rows: 24 }, + (event) => { + outputs.push(event); + } + ); + // Give some time for the stream to process - await new Promise(resolve => setTimeout(resolve, 10)); + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(outputs.length).toBeGreaterThan(0); + expect(outputs[0]).toEqual({ + out: "Hello World", + exitCode: undefined, + }); + + disposable.dispose(); - unsubscribe.dispose(); - // Note: Due to the async nature of the stream, we can't easily test the actual firing - // without more complex mocking, but we can verify the structure exists + expect(pintApi.getExecOutput).toHaveBeenCalledWith({ + client: mockApiClient, + path: { id: "exec-123" }, + query: { lastSequence: 0 }, + signal: expect.any(AbortSignal), + headers: { + Accept: "text/event-stream", + }, + }); + }); + + it("should handle output with exit code", async () => { + const outputStream = (async function* (): AsyncGenerator< + string, + any, + unknown + > { + yield 'data:{"type":"stdout","output":"Done","sequence":1,"timestamp":"2023-01-01T12:00:00Z","exitCode":0}'; + })(); + + vi.mocked(pintApi.getExecOutput).mockResolvedValue({ + ...createMockResponse({}), + stream: outputStream, + }); + + const outputs: any[] = []; + const disposable = client.subscribeOutput( + "exec-123", + { cols: 80, rows: 24 }, + (event) => { + outputs.push(event); + } + ); + + // Give some time for the stream to process + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect(outputs.length).toBeGreaterThan(0); + expect(outputs[0]).toEqual({ + out: "Done", + exitCode: 0, + }); + + disposable.dispose(); + }); + + it("should return disposable for cleanup", () => { + const outputStream = (async function* (): AsyncGenerator< + string, + any, + unknown + > {})(); + + vi.mocked(pintApi.getExecOutput).mockResolvedValue({ + ...createMockResponse({}), + stream: outputStream, + }); + + const disposable = client.subscribeOutput( + "exec-123", + { cols: 80, rows: 24 }, + () => {} + ); + + expect(disposable).toBeDefined(); + expect(typeof disposable.dispose).toBe("function"); + + disposable.dispose(); }); }); -}); \ No newline at end of file +}); diff --git a/tests/sandbox-creation.test.ts b/tests/sandbox-creation.test.ts index e93444b..22ab9e7 100644 --- a/tests/sandbox-creation.test.ts +++ b/tests/sandbox-creation.test.ts @@ -1,80 +1,89 @@ -import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import nock from 'nock' -import { CodeSandbox } from '../src/index' -import { - mockForkSandboxSuccess, - mockStartVMSuccess, - setupTestEnvironment, - cleanupTestEnvironment -} from './test-utils' - -describe('Sandbox Creation', () => { +import { describe, it, expect, beforeEach, afterEach } from "vitest"; +import nock from "nock"; +import { CodeSandbox } from "../src/index"; +import { + mockForkSandboxSuccess, + mockStartVMSuccess, + setupTestEnvironment, + cleanupTestEnvironment, +} from "./test-utils"; + +describe("Sandbox Creation", () => { beforeEach(() => { - setupTestEnvironment() - }) + setupTestEnvironment(); + }); afterEach(() => { - cleanupTestEnvironment() - }) + cleanupTestEnvironment(); + }); - it('should successfully create and start a sandbox', async () => { + it("should successfully create and start a sandbox", async () => { // Mock the fork sandbox API call (pcz35m is the default template) - const forkScope = mockForkSandboxSuccess('test-sandbox-123', { - title: 'Test Sandbox', - description: 'Integration test sandbox', + const forkScope = mockForkSandboxSuccess("test-sandbox-123", { + title: "Test Sandbox", + description: "Integration test sandbox", privacy: 1, - tags: ['integration-test', 'sdk'] - }) + tags: ["integration-test", "sdk"], + }); // Mock the start VM API call - use regex to match any ID - const startScope = mockStartVMSuccess('test-sandbox-123') + const startScope = mockStartVMSuccess("test-sandbox-123"); // Initialize SDK - const sdk = new CodeSandbox() - + const sdk = new CodeSandbox(); + // Create sandbox const sandbox = await sdk.sandboxes.create({ - title: 'Test Sandbox', - description: 'Integration test sandbox', - privacy: 'unlisted', - tags: ['integration-test'] - }) + title: "Test Sandbox", + description: "Integration test sandbox", + privacy: "unlisted", + tags: ["integration-test"], + }); // Verify sandbox was created successfully - expect(sandbox).toBeDefined() - expect(sandbox.id).toBe('test-sandbox-123') - + expect(sandbox).toBeDefined(); + expect(sandbox.id).toBe("test-sandbox-123"); + // Verify all API calls were made - expect(forkScope.isDone()).toBe(true) - expect(startScope.isDone()).toBe(true) - }, 10000) // 10 second timeout for integration test + expect(forkScope.isDone()).toBe(true); + expect(startScope.isDone()).toBe(true); + }, 10000); // 10 second timeout for integration test - it('should use default template when no id is provided', async () => { + it("should use default template when no id is provided", async () => { // Mock default template call - pcz35m is the default template - const forkScope = mockForkSandboxSuccess('default-sandbox-456') + // Default privacy is "public-hosts" which maps to privacy: 2, private_preview: false + const forkScope = mockForkSandboxSuccess("default-sandbox-456", { + privacy: 2, + private_preview: false, + }); + + const startScope = mockStartVMSuccess("default-sandbox-456"); - const startScope = mockStartVMSuccess('default-sandbox-456') + const sdk = new CodeSandbox(); - const sdk = new CodeSandbox() - // Create sandbox without specifying template id - const sandbox = await sdk.sandboxes.create() - - expect(sandbox).toBeDefined() - expect(sandbox.id).toBe('default-sandbox-456') - expect(forkScope.isDone()).toBe(true) - expect(startScope.isDone()).toBe(true) - }) - - it('should handle API errors gracefully', async () => { - // Mock fork sandbox failure - nock('https://api.codesandbox.io') - .post('/sandbox/pcz35m/fork') - .reply(500, { message: 'Internal server error' }) - - const sdk = new CodeSandbox() - + const sandbox = await sdk.sandboxes.create(); + + expect(sandbox).toBeDefined(); + expect(sandbox.id).toBe("default-sandbox-456"); + expect(forkScope.isDone()).toBe(true); + expect(startScope.isDone()).toBe(true); + }); + + it("should handle API errors gracefully", async () => { + // Mock fork sandbox failure with expected request body + nock("https://api.codesandbox.io") + .post("/sandbox/pcz35m/fork", { + privacy: 2, + tags: ["sdk"], + path: "/SDK", + private_preview: false + }) + .reply(500, { message: "Internal server error" }); + + const sdk = new CodeSandbox(); + // Expect the creation to throw an error - await expect(sdk.sandboxes.create()).rejects.toThrow() - }) -}) \ No newline at end of file + await expect(sdk.sandboxes.create()).rejects.toThrow(); + }); +}); diff --git a/tests/sandbox-retry-behavior.test.ts b/tests/sandbox-retry-behavior.test.ts index 4e03c80..f9e2853 100644 --- a/tests/sandbox-retry-behavior.test.ts +++ b/tests/sandbox-retry-behavior.test.ts @@ -20,10 +20,15 @@ describe('Create operation retry behavior', () => { it('should fail immediately on fork API error (no retry for fork)', async () => { let forkRequestCount = 0 - + // Mock fork to fail once - should fail immediately since fork doesn't retry const forkScope = nock('https://api.codesandbox.io') - .post('/sandbox/pcz35m/fork') + .post('/sandbox/pcz35m/fork', { + privacy: 2, + tags: ['sdk'], + path: '/SDK', + private_preview: false + }) .reply(500, () => { forkRequestCount++ return { error: { errors: ['Fork failed'] } } @@ -44,9 +49,12 @@ describe('Create operation retry behavior', () => { it('should retry start VM failures and eventually succeed', async () => { let startVMRequestCount = 0 - - // Mock successful fork - const forkScope = mockForkSandboxSuccess('test-sandbox-start-retry') + + // Mock successful fork with default privacy settings + const forkScope = mockForkSandboxSuccess('test-sandbox-start-retry', { + privacy: 2, + private_preview: false, + }) // Mock start VM to fail twice const failureScope = nock('https://api.codesandbox.io') @@ -92,9 +100,12 @@ describe('Create operation retry behavior', () => { it('should fail create after start VM exhausts all retries', async () => { let startVMRequestCount = 0 - - // Mock successful fork - const forkScope = mockForkSandboxSuccess('test-sandbox-start-fail') + + // Mock successful fork with default privacy settings + const forkScope = mockForkSandboxSuccess('test-sandbox-start-fail', { + privacy: 2, + private_preview: false, + }) // Mock start VM to fail all 3 retry attempts const failureScope = nock('https://api.codesandbox.io') @@ -117,9 +128,12 @@ describe('Create operation retry behavior', () => { it('should validate retry timing for start VM failures', async () => { let startVMRequestCount = 0 - - // Mock successful fork - const forkScope = mockForkSandboxSuccess('test-sandbox-timing') + + // Mock successful fork with default privacy settings + const forkScope = mockForkSandboxSuccess('test-sandbox-timing', { + privacy: 2, + private_preview: false, + }) // Mock start VM to fail twice const failureScope = nock('https://api.codesandbox.io') diff --git a/tests/test-utils.ts b/tests/test-utils.ts index 6ad1cf8..219c219 100644 --- a/tests/test-utils.ts +++ b/tests/test-utils.ts @@ -1,99 +1,150 @@ -import nock from 'nock' +import nock from "nock"; -export const mockForkSandboxSuccess = (sandboxId: string, options?: { - title?: string - description?: string - privacy?: number - tags?: string[] -}) => { - return nock('https://api.codesandbox.io') - .post('/sandbox/pcz35m/fork', { - privacy: options?.privacy ?? 1, - ...(options?.title && { title: options.title }), - ...(options?.description && { description: options.description }), - tags: options?.tags ?? ['sdk'], - path: '/SDK' - }) +export const mockForkSandboxSuccess = ( + sandboxId: string, + options?: { + title?: string; + description?: string; + privacy?: number; + tags?: string[]; + private_preview?: boolean; + } +) => { + const requestBody: Record = { + privacy: options?.privacy ?? 1, + ...(options?.title && { title: options.title }), + ...(options?.description && { description: options.description }), + tags: options?.tags ?? ["sdk"], + path: "/SDK", + }; + + // Only add private_preview if explicitly provided + if (options?.private_preview !== undefined) { + requestBody.private_preview = options.private_preview; + } + + return nock("https://api.codesandbox.io") + .post("/sandbox/pcz35m/fork", requestBody) .reply(200, { data: { id: sandboxId, - title: options?.title ?? 'Test Sandbox', + title: options?.title ?? "Test Sandbox", description: options?.description, privacy: options?.privacy ?? 1, - tags: options?.tags ?? ['sdk'], - created_at: '2025-01-21T12:00:00Z', - updated_at: '2025-01-21T12:00:00Z' - } - }) -} + tags: options?.tags ?? ["sdk"], + created_at: "2025-01-21T12:00:00Z", + updated_at: "2025-01-21T12:00:00Z", + }, + }); +}; -export const mockStartVMSuccess = (sandboxId: string, bootupType: 'CLEAN' | 'RESUME' = 'CLEAN') => { - return nock('https://api.codesandbox.io') +export const mockStartVMSuccess = ( + sandboxId: string, + bootupType: "CLEAN" | "RESUME" = "CLEAN" +) => { + return nock("https://api.codesandbox.io") .post(/\/vm\/.*\/start/) .reply(200, { data: { bootup_type: bootupType, - cluster: 'test-cluster', + cluster: "test-cluster", pitcher_url: `wss://pitcher.codesandbox.io/${sandboxId}`, - workspace_path: '/project/sandbox', - user_workspace_path: '/project/sandbox', - pitcher_manager_version: '1.0.0', - pitcher_version: '1.0.0', - latest_pitcher_version: '1.0.0', - pitcher_token: `pitcher-token-${sandboxId.split('-').pop()}` - } - }) -} + workspace_path: "/project/sandbox", + user_workspace_path: "/project/sandbox", + pitcher_manager_version: "1.0.0", + pitcher_version: "1.0.0", + latest_pitcher_version: "1.0.0", + pitcher_token: `pitcher-token-${sandboxId.split("-").pop()}`, + }, + }); +}; -export const mockStartVMFailure = (times: number = 1, errorMessage: string = 'Start failed') => { - return nock('https://api.codesandbox.io') +export const mockStartVMFailure = ( + times: number = 1, + errorMessage: string = "Start failed" +) => { + return nock("https://api.codesandbox.io") .post(/\/vm\/.*\/start/) .times(times) - .reply(500, { error: { errors: [errorMessage] } }) -} + .reply(500, { error: { errors: [errorMessage] } }); +}; export const mockHibernateSuccess = (sandboxId: string) => { - return nock('https://api.codesandbox.io') + return nock("https://api.codesandbox.io") .post(`/vm/${sandboxId}/hibernate`) .reply(200, { data: { - success: true - } - }) -} + success: true, + }, + }); +}; -export const mockHibernateFailure = (sandboxId: string, times: number = 1, errorMessage: string = 'Server error') => { - return nock('https://api.codesandbox.io') +export const mockHibernateFailure = ( + sandboxId: string, + times: number = 1, + errorMessage: string = "Server error" +) => { + return nock("https://api.codesandbox.io") .post(`/vm/${sandboxId}/hibernate`) .times(times) - .reply(500, { error: { errors: [errorMessage] } }) -} + .reply(500, { error: { errors: [errorMessage] } }); +}; export const mockShutdownSuccess = (sandboxId: string) => { - return nock('https://api.codesandbox.io') + return nock("https://api.codesandbox.io") .post(`/vm/${sandboxId}/shutdown`) .reply(200, { data: { - success: true - } - }) -} + success: true, + }, + }); +}; -export const mockShutdownFailure = (sandboxId: string, times: number = 1, errorMessage: string = 'Shutdown failed') => { - return nock('https://api.codesandbox.io') +export const mockShutdownFailure = ( + sandboxId: string, + times: number = 1, + errorMessage: string = "Shutdown failed" +) => { + return nock("https://api.codesandbox.io") .post(`/vm/${sandboxId}/shutdown`) .times(times) - .reply(500, { error: { errors: [errorMessage] } }) -} + .reply(500, { error: { errors: [errorMessage] } }); +}; export const setupTestEnvironment = () => { - process.env.CSB_API_KEY = 'csb_test_key_123' - nock.cleanAll() -} + process.env.CSB_API_KEY = "csb_test_key_123"; + nock.cleanAll(); +}; export const cleanupTestEnvironment = () => { if (!nock.isDone()) { - console.error('Unused nock interceptors:', nock.pendingMocks()) + console.error("Unused nock interceptors:", nock.pendingMocks()); + } + nock.cleanAll(); +}; + +/** + * Properly cleanup test sandbox with correct sequencing: + * 1. Wait for client cleanup (disconnect and dispose) + * 2. Wait for shutdown with 10 second timeout + * 3. Fire-and-forget delete (with small delay to ensure request goes through) + */ +export const cleanupTestSandbox = async ( + client: any | undefined, + sandboxId: string | undefined, + sdk: any +): Promise => { + if (client) { + try { + await client.disconnect(); + client.dispose(); + } catch (error) { + console.error("Failed to disconnect client:", error); + } + } + + if (sandboxId) { + await sdk.sandboxes.shutdown(sandboxId); + await sdk.sandboxes.delete(sandboxId); } - nock.cleanAll() -} \ No newline at end of file +}; diff --git a/vitest.config.ts b/vitest.config.ts index cba2eed..6cd2bf3 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,6 +4,7 @@ export default defineConfig({ test: { environment: 'node', include: ['tests/**/*.test.ts'], + testTimeout: 10000, // Doubled from default 5000ms }, define: { CSB_SDK_VERSION: JSON.stringify('2.1.0-rc.4'),