diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c92a0847ff..96d32e82de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: - name: Set up Node.js with npm cache uses: actions/setup-node@v6 with: - node-version: 22.12.0 + node-version: '24' cache: npm - name: Install dependencies @@ -43,5 +43,11 @@ jobs: - name: Test run: npm test - - name: Build + - name: Build app run: npm run build + + - name: Build library + run: npm run build:lib + + - name: Verify package contents + run: npm pack --dry-run diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml new file mode 100644 index 0000000000..5e8d29bbb7 --- /dev/null +++ b/.github/workflows/npm-publish.yml @@ -0,0 +1,94 @@ +name: Publish to npm + +on: + push: + tags: + - 'v*' + +concurrency: + group: npm-publish-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + id-token: write + +jobs: + publish: + name: Publish to npm + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + # Trusted publishing requires Node 22.14.0+ and npm 11.5.1+ + # See: https://docs.npmjs.com/trusted-publishers/ + - name: Set up Node.js for npm trusted publishing + uses: actions/setup-node@v6 + with: + node-version: '24' + cache: npm + registry-url: https://registry.npmjs.org + + - name: Verify npm version supports trusted publishing + run: | + echo "Node version: $(node --version)" + echo "npm version: $(npm --version)" + NPM_VERSION=$(npm --version) + NPM_MAJOR=$(echo $NPM_VERSION | cut -d. -f1) + NPM_MINOR=$(echo $NPM_VERSION | cut -d. -f2) + if [ "$NPM_MAJOR" -lt 11 ] || ([ "$NPM_MAJOR" -eq 11 ] && [ "$NPM_MINOR" -lt 5 ]); then + echo "Error: npm 11.5.1+ required for trusted publishing, got $NPM_VERSION" + exit 1 + fi + echo "✓ npm $NPM_VERSION meets trusted publishing requirements" + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + - name: Build app + run: npm run build + + - name: Build library + run: npm run build:lib + + - name: Verify package contents + run: npm pack --dry-run + + - name: Validate package version matches release tag + run: | + PACKAGE_VERSION=$(node -p "require('./package.json').version") + TAG_VERSION=${GITHUB_REF#refs/tags/v} + echo "Package version: $PACKAGE_VERSION" + echo "Release tag version: $TAG_VERSION" + if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then + echo "Error: package.json version ($PACKAGE_VERSION) doesn't match release tag ($TAG_VERSION)" + exit 1 + fi + echo "✓ Version $PACKAGE_VERSION matches release tag" + + - name: Determine npm tag + id: npm-tag + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/v} + if [[ "$TAG_VERSION" == *"alpha"* ]]; then + echo "tag=alpha" >> $GITHUB_OUTPUT + echo "Publishing with tag: alpha" + elif [[ "$TAG_VERSION" == *"beta"* ]]; then + echo "tag=beta" >> $GITHUB_OUTPUT + echo "Publishing with tag: beta" + elif [[ "$TAG_VERSION" == *"rc"* ]]; then + echo "tag=rc" >> $GITHUB_OUTPUT + echo "Publishing with tag: rc" + else + echo "tag=latest" >> $GITHUB_OUTPUT + echo "Publishing with tag: latest" + fi + + - name: Publish to npm with provenance + run: npm publish --access public --provenance --tag ${{ steps.npm-tag.outputs.tag }} diff --git a/.github/workflows/sdk-version-sync.yml b/.github/workflows/sdk-version-sync.yml index 7441045d91..efe97d8fdf 100644 --- a/.github/workflows/sdk-version-sync.yml +++ b/.github/workflows/sdk-version-sync.yml @@ -55,7 +55,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v6 with: - node-version: 22.12.0 + node-version: '24' - name: Log trigger info run: | diff --git a/.gitignore b/.gitignore index 945375afde..ef996a7db8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ src/i18n/declaration.ts .envrc node_modules/ build/ +dist/ /test-results/ @@ -14,3 +15,4 @@ build/ .react-router/ ralph/ .vercel +*.tgz diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..8ab0df2d7f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,29 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [1.0.0-alpha.2] - 2025-05-11 + +### Added + +- Initial npm package release of `@openhands/agent-canvas` +- CLI entry point (`npx @openhands/agent-canvas`) to run full stack with Docker +- Library build mode with component barrel exports +- Subpath exports for modular imports: + - `@openhands/agent-canvas/browser` + - `@openhands/agent-canvas/conversation` + - `@openhands/agent-canvas/files` + - `@openhands/agent-canvas/settings` + - `@openhands/agent-canvas/sidebar` + - `@openhands/agent-canvas/terminal` + - `@openhands/agent-canvas/i18n` +- TypeScript type declarations +- GitHub Actions workflow for automated npm publishing (OIDC trusted publishing) + +[Unreleased]: https://github.com/OpenHands/agent-canvas/compare/v1.0.0-alpha.2...HEAD +[1.0.0-alpha.2]: https://github.com/OpenHands/agent-canvas/releases/tag/v1.0.0-alpha.2 diff --git a/README.md b/README.md index 9ee5224443..83b9c44c82 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,42 @@ The Agent Server is often paired with an [Automation Server](https://github.com/ image +## npm Package + +Agent Canvas is also available as an npm package for embedding in your own applications: + +```bash +npm install @openhands/agent-canvas +``` + +### Usage + +Import the full package or specific components: + +```typescript +// Full package +import { AgentServerUIProviders } from '@openhands/agent-canvas'; + +// Individual component packages +import { BrowserPanel } from '@openhands/agent-canvas/browser'; +import { ChatPanel } from '@openhands/agent-canvas/conversation'; +import { FileExplorer } from '@openhands/agent-canvas/files'; +import { TerminalPanel } from '@openhands/agent-canvas/terminal'; +``` + +### Available Subpath Exports + +| Subpath | Description | +|---------|-------------| +| `@openhands/agent-canvas` | Main entry with providers and core components | +| `@openhands/agent-canvas/browser` | Browser/preview panel components | +| `@openhands/agent-canvas/conversation` | Chat interface and message components | +| `@openhands/agent-canvas/files` | File explorer and editor components | +| `@openhands/agent-canvas/settings` | Settings screens and forms | +| `@openhands/agent-canvas/sidebar` | Sidebar navigation components | +| `@openhands/agent-canvas/terminal` | Terminal emulator component | +| `@openhands/agent-canvas/i18n` | Internationalization resources | + ## More documentation For contributor and developer workflows, including frontend-only mode, mock mode, environment variables, and build/test commands, see [DEVELOPMENT.md](./DEVELOPMENT.md). diff --git a/bin/agent-canvas.mjs b/bin/agent-canvas.mjs new file mode 100755 index 0000000000..44ff0f6d68 --- /dev/null +++ b/bin/agent-canvas.mjs @@ -0,0 +1,108 @@ +#!/usr/bin/env node +/** + * CLI entry point for @openhands/agent-canvas + * + * Runs the full Agent Canvas stack using Docker for the agent-server: + * - Agent-server runs in Docker container + * - Automation backend via uvx + * - Pre-built static frontend (not Vite dev server) + * + * This is the production equivalent of `npm run dev` - it runs the full stack + * but serves pre-built static assets instead of the Vite dev server. + */ + +import { existsSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const BUILD_DIR = join(__dirname, "..", "build", "client"); + +// Check for help flag first +const args = process.argv.slice(2); +if (args.includes("-h") || args.includes("--help")) { + console.log(` +@openhands/agent-canvas - Run the Agent Canvas UI with agent-server (Docker) + +Runs the full stack with agent-server in Docker, automation backend via uvx, +and serves pre-built static frontend assets. + +USAGE: + npx @openhands/agent-canvas [options] + +REQUIRED: + PROJECT_PATH Path to your projects directory (mounted into container) + +OPTIONS: + -p, --port Ingress port (default: 8000) + -h, --help Show this help message + +ENVIRONMENT VARIABLES: + PROJECT_PATH Required: path to your projects directory + OH_SECRET_KEY Secret key for encrypting settings + OH_AGENT_SERVER_GIT_REF Git ref for agent-server Docker image tag + OH_AGENT_SERVER_LOCAL_PATH Path to local SDK checkout (for development) + OH_MOUNT_HOST_HOME Set to "1" to mount entire home directory + +Note: LLM settings are configured through the web UI settings page, +not environment variables. + +EXAMPLES: + # Start full stack (requires PROJECT_PATH) + PROJECT_PATH=/path/to/projects npx @openhands/agent-canvas + + # Use a specific port + PROJECT_PATH=/path/to/projects npx @openhands/agent-canvas --port 3000 + + # Use local SDK checkout for development + PROJECT_PATH=/path/to/projects OH_AGENT_SERVER_LOCAL_PATH=/path/to/sdk npx @openhands/agent-canvas + +For more options, see: node scripts/dev-docker.mjs --help +`); + process.exit(0); +} + +// Check build exists before doing anything else +if (!existsSync(BUILD_DIR)) { + console.error(` +Error: No build found at ${BUILD_DIR} + +This package needs to be built first. If you installed from npm, +this is a packaging error. If running from source: + + npm install + npm run build +`); + process.exit(1); +} + +// Import dev-docker's dependencies and run with static mode +let main, checkDockerPrereqs, startAgentServerDocker, CONTAINER_WORKSPACES_DIR; +try { + ({ main } = await import("../scripts/dev-with-automation.mjs")); + ({ + checkDockerPrereqs, + startAgentServerDocker, + CONTAINER_WORKSPACES_DIR, + } = await import("../scripts/dev-docker.mjs")); +} catch (err) { + console.error("Failed to load required scripts. Try reinstalling:"); + console.error(" npm install -g @openhands/agent-canvas@latest"); + console.error(`\nError: ${err.message}`); + process.exit(1); +} + +main({ + bannerTitle: "Agent Canvas", + extraPrereqs: checkDockerPrereqs, + startAgentServer: startAgentServerDocker, + viteWorkingDir: CONTAINER_WORKSPACES_DIR, + staticMode: true, + staticDir: BUILD_DIR, +}).catch((err) => { + console.error(`Fatal error: ${err.message}`); + if (err.stack) { + console.error(err.stack); + } + process.exit(1); +}); diff --git a/package-lock.json b/package-lock.json index ce711f1349..5a5939035a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@openhands/agent-canvas", - "version": "1.6.0", + "version": "1.0.0-alpha.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@openhands/agent-canvas", - "version": "1.6.0", + "version": "1.0.0-alpha.2", "license": "MIT", "dependencies": { "@heroui/react": "2.8.10", @@ -53,6 +53,9 @@ "vite": "8.0.10", "zustand": "5.0.12" }, + "bin": { + "agent-canvas": "bin/agent-canvas.mjs" + }, "devDependencies": { "@eslint/eslintrc": "3.3.1", "@eslint/js": "9.39.4", diff --git a/package.json b/package.json index 501a5910fc..00edbde559 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,21 @@ { "name": "@openhands/agent-canvas", - "version": "1.6.0", + "version": "1.0.0-alpha.2", + "description": "Agent Canvas UI for OpenHands - run AI coding agents with a visual interface", "license": "MIT", "private": false, "type": "module", + "repository": { + "type": "git", + "url": "https://github.com/OpenHands/agent-canvas" + }, + "homepage": "https://github.com/OpenHands/agent-canvas#readme", + "bugs": { + "url": "https://github.com/OpenHands/agent-canvas/issues" + }, + "bin": { + "agent-canvas": "bin/agent-canvas.mjs" + }, "engines": { "node": ">=22.12.0" }, @@ -75,7 +87,7 @@ "prelint": "npm run make-i18n", "lint": "npm run typecheck && eslint src && prettier --check src/**/*.{ts,tsx}", "lint:fix": "eslint src --fix && prettier --write src/**/*.{ts,tsx}", - "prepare": "cd .. && husky frontend/.husky", + "prepare": "[ -d '../.git' ] && cd .. && husky frontend/.husky || true", "typecheck": "react-router typegen && tsc", "typecheck:staged": "react-router typegen && npx tsc --noEmit --skipLibCheck", "check-translation-completeness": "node scripts/check-translation-completeness.cjs", @@ -154,7 +166,10 @@ "module": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ - "dist" + "dist", + "bin", + "build", + "scripts" ], "exports": { ".": { diff --git a/scripts/dev-with-automation.mjs b/scripts/dev-with-automation.mjs index 4a1cc3b8ba..af3f3bd0c4 100644 --- a/scripts/dev-with-automation.mjs +++ b/scripts/dev-with-automation.mjs @@ -38,7 +38,7 @@ */ import { spawn, spawnSync } from "node:child_process"; -import { mkdirSync } from "node:fs"; +import { mkdirSync, existsSync } from "node:fs"; import { join, resolve, dirname } from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { homedir } from "node:os"; @@ -116,6 +116,8 @@ function parseArgs() { automationGitRef: null, automationRepo: null, verbose: false, + static: false, + staticDir: null, }; for (let i = 0; i < args.length; i++) { @@ -134,6 +136,12 @@ function parseArgs() { case "--verbose": config.verbose = true; break; + case "--static": + config.static = true; + break; + case "--static-dir": + config.staticDir = args[++i]; + break; case "-h": case "--help": showHelp(); @@ -689,12 +697,21 @@ async function main(options = {}) { startAgentServer: startAgentServerOverride, extraPrereqs, viteWorkingDir, + staticMode: staticModeOverride, + staticDir: staticDirOverride, } = options; const args = parseArgs(); + // Allow options to override CLI args (for bin/agent-canvas.mjs) + const useStaticMode = staticModeOverride ?? args.static; + const staticDir = staticDirOverride ?? args.staticDir ?? join(projectRoot, "build", "client"); + + const modeLabel = useStaticMode ? "(Static)" : ""; + const titleWithMode = modeLabel ? `${bannerTitle} ${modeLabel}` : bannerTitle; + console.log(""); - console.log(`${c.cyan}${c.bold}${bannerTitle}${c.reset}`); + console.log(`${c.cyan}${c.bold}${titleWithMode}${c.reset}`); console.log(""); // Setup phase @@ -702,6 +719,13 @@ async function main(options = {}) { // backend runs via uvx; only the agent-server is dockerized.) checkPrerequisites(); + // In static mode, verify build exists + if (useStaticMode && !existsSync(staticDir)) { + logError(`Static directory not found: ${staticDir}`); + logError(`Run 'npm run build' first to create the static files.`); + process.exit(1); + } + // Build config with dynamic port allocation const config = await buildConfig(args); if (viteWorkingDir) config.viteWorkingDir = viteWorkingDir; @@ -736,8 +760,12 @@ async function main(options = {}) { // 3. Start automation backend startAutomationBackend(config); - // 4. Start Vite dev server (no proxy config needed - ingress handles routing) - startVite(config); + // 4. Start frontend server (Vite dev server OR static server) + if (useStaticMode) { + startStaticFrontend(config, staticDir); + } else { + startVite(config); + } // 5. Wait for services to be ready await delay(2000); @@ -751,6 +779,35 @@ async function main(options = {}) { printBanner(config); } +function startStaticFrontend(config, staticDir) { + logService("static", `Starting on port ${config.vitePort}...`, c.magenta); + logService("static", `Serving from: ${staticDir}`, c.dim); + + const staticServerScript = join(projectRoot, "scripts", "static-server.mjs"); + spawnService( + "static", + "node", + [ + staticServerScript, + "--dir", staticDir, + "--host", "0.0.0.0", + "--port", String(config.vitePort), + // Proxy routes to backends (same as ingress but for direct access to vitePort) + "--route", `/api/automation=http://localhost:${config.autoBackendPort}`, + "--route", `/api=http://localhost:${config.agentServerPort}`, + "--route", `/sockets=http://localhost:${config.agentServerPort}`, + "--route", `/server_info=http://localhost:${config.agentServerPort}`, + "--route", `/health=http://localhost:${config.agentServerPort}`, + "--route", `/ready=http://localhost:${config.agentServerPort}`, + "--route", `/alive=http://localhost:${config.agentServerPort}`, + ], + { + cwd: config.canvasPath, + color: c.magenta, + } + ); +} + // ═══════════════════════════════════════════════════════════════════════════ // Exports for testing // ═══════════════════════════════════════════════════════════════════════════