diff --git a/.changeset/clear-pears-know.md b/.changeset/clear-pears-know.md new file mode 100644 index 0000000..8139ee0 --- /dev/null +++ b/.changeset/clear-pears-know.md @@ -0,0 +1,5 @@ +--- +"@plotday/sdk": patch +--- + +Added: Progress updates for agent generate and deploy diff --git a/.changeset/long-baths-invent.md b/.changeset/long-baths-invent.md new file mode 100644 index 0000000..4f74c40 --- /dev/null +++ b/.changeset/long-baths-invent.md @@ -0,0 +1,5 @@ +--- +"@plotday/sdk": patch +--- + +Added: Install latest SDK package after generate diff --git a/sdk/cli/commands/create.ts b/sdk/cli/commands/create.ts index a0d7fd1..2ce8742 100644 --- a/sdk/cli/commands/create.ts +++ b/sdk/cli/commands/create.ts @@ -3,6 +3,7 @@ import * as fs from "fs"; import * as path from "path"; import prompts from "prompts"; import * as out from "../utils/output"; +import { detectPackageManager } from "../utils/packageManager"; interface CreateOptions { dir?: string; @@ -10,29 +11,6 @@ interface CreateOptions { displayName?: string; } -/** - * Detects the package manager being used - * Checks for lock files and npm_config_user_agent - */ -function detectPackageManager(): string { - // Check npm_config_user_agent first (set by npm, yarn, pnpm) - const userAgent = process.env.npm_config_user_agent; - if (userAgent) { - if (userAgent.includes("yarn")) return "yarn"; - if (userAgent.includes("pnpm")) return "pnpm"; - if (userAgent.includes("npm")) return "npm"; - } - - // Check for lock files in current directory - const cwd = process.cwd(); - if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm"; - if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn"; - if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm"; - - // Default to npm - return "npm"; -} - export async function createCommand(options: CreateOptions) { out.header("Create a new Plot agent"); diff --git a/sdk/cli/commands/deploy.ts b/sdk/cli/commands/deploy.ts index e925f6f..9f3cfc1 100644 --- a/sdk/cli/commands/deploy.ts +++ b/sdk/cli/commands/deploy.ts @@ -6,6 +6,7 @@ import prompts from "prompts"; import * as out from "../utils/output"; import { getGlobalTokenPath } from "../utils/token"; import { bundleAgent } from "../utils/bundle"; +import { handleSSEStream } from "../utils/sse"; interface DeployOptions { dir: string; @@ -257,6 +258,7 @@ export async function deployCommand(options: DeployOptions) { method: "POST", headers: { "Content-Type": "application/json", + Accept: "text/event-stream", Authorization: `Bearer ${deployToken}`, }, body: JSON.stringify(requestBody), @@ -271,7 +273,12 @@ export async function deployCommand(options: DeployOptions) { process.exit(1); } - const result = (await response.json()) as any; + // Handle SSE stream with progress updates + const result = (await handleSSEStream(response, { + onProgress: (message) => { + out.progress(message); + }, + })) as any; // Handle dryRun response if (options.dryRun) { diff --git a/sdk/cli/commands/generate.ts b/sdk/cli/commands/generate.ts index 2bf4f0d..cfc0ca9 100644 --- a/sdk/cli/commands/generate.ts +++ b/sdk/cli/commands/generate.ts @@ -1,10 +1,13 @@ +import { execSync } from "child_process"; import * as dotenv from "dotenv"; import * as fs from "fs"; import * as path from "path"; import prompts from "prompts"; import * as out from "../utils/output"; +import { detectPackageManager } from "../utils/packageManager"; import { getGlobalTokenPath } from "../utils/token"; +import { handleSSEStream } from "../utils/sse"; interface GenerateOptions { dir: string; @@ -175,6 +178,7 @@ export async function generateCommand(options: GenerateOptions) { method: "POST", headers: { "Content-Type": "application/json", + Accept: "text/event-stream", Authorization: `Bearer ${deployToken}`, }, body: JSON.stringify({ spec: specContent }), @@ -189,7 +193,12 @@ export async function generateCommand(options: GenerateOptions) { process.exit(1); } - const source = (await response.json()) as AgentSource; + // Handle SSE stream with progress updates + const source = (await handleSSEStream(response, { + onProgress: (message) => { + out.progress(message); + }, + })) as AgentSource; // Create agent directory if it doesn't exist if (!fs.existsSync(agentPath)) { @@ -295,6 +304,41 @@ export async function generateCommand(options: GenerateOptions) { writeFile(filePath, content); } + out.blank(); + + // Detect package manager and install dependencies + const packageManager = detectPackageManager(); + + // Update @plotday/sdk to latest and install packages + try { + out.progress("Updating @plotday/sdk to latest version..."); + + const updateCommand = + packageManager === "npm" ? "npm install @plotday/sdk@latest" : + packageManager === "pnpm" ? "pnpm add @plotday/sdk@latest" : + "yarn add @plotday/sdk@latest"; + + execSync(updateCommand, { cwd: agentPath, stdio: "ignore" }); + + out.progress("Installing dependencies..."); + + const installCommand = + packageManager === "yarn" ? "yarn" : + `${packageManager} install`; + + execSync(installCommand, { cwd: agentPath, stdio: "ignore" }); + + out.success("Dependencies installed successfully!"); + } catch (error) { + out.warning( + "Couldn't install dependencies", + [ + `Run '${packageManager === "npm" ? "npm install @plotday/sdk@latest" : packageManager === "pnpm" ? "pnpm add @plotday/sdk@latest" : "yarn add @plotday/sdk@latest"}' in ${options.dir}`, + `Then run '${packageManager === "yarn" ? "yarn" : `${packageManager} install`}'` + ] + ); + } + out.blank(); out.success("Agent generated successfully!"); diff --git a/sdk/cli/utils/packageManager.ts b/sdk/cli/utils/packageManager.ts new file mode 100644 index 0000000..f5ece5c --- /dev/null +++ b/sdk/cli/utils/packageManager.ts @@ -0,0 +1,25 @@ +import * as fs from "fs"; +import * as path from "path"; + +/** + * Detects the package manager being used + * Checks for npm_config_user_agent and lock files + */ +export function detectPackageManager(): string { + // Check npm_config_user_agent first (set by npm, yarn, pnpm) + const userAgent = process.env.npm_config_user_agent; + if (userAgent) { + if (userAgent.includes("yarn")) return "yarn"; + if (userAgent.includes("pnpm")) return "pnpm"; + if (userAgent.includes("npm")) return "npm"; + } + + // Check for lock files in current directory + const cwd = process.cwd(); + if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm"; + if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn"; + if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm"; + + // Default to npm + return "npm"; +} diff --git a/sdk/cli/utils/sse.ts b/sdk/cli/utils/sse.ts new file mode 100644 index 0000000..eeaf439 --- /dev/null +++ b/sdk/cli/utils/sse.ts @@ -0,0 +1,116 @@ +/** + * Server-Sent Events (SSE) client utilities + */ + +export interface SSEEvent { + event: string; + data: any; + id?: string; +} + +export interface SSEHandlers { + onProgress?: (message: string) => void; + onResult?: (data: any) => void; + onError?: (error: string) => void; +} + +/** + * Parse and handle SSE response stream + */ +export async function handleSSEStream( + response: Response, + handlers: SSEHandlers +): Promise { + if (!response.body) { + throw new Error("Response has no body"); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let result: any = null; + + try { + while (true) { + const { done, value } = await reader.read(); + + if (done) { + break; + } + + // Decode chunk and add to buffer + buffer += decoder.decode(value, { stream: true }); + + // Process complete messages in buffer + const lines = buffer.split("\n"); + + // Keep incomplete message in buffer + buffer = lines.pop() || ""; + + let currentEvent: Partial = {}; + + for (const line of lines) { + // Empty line marks end of event + if (line.trim() === "") { + if (currentEvent.event && currentEvent.data !== undefined) { + // Parse data if it's JSON + let parsedData = currentEvent.data; + try { + parsedData = JSON.parse(currentEvent.data); + } catch { + // Not JSON, use as-is + } + + // Handle event based on type + switch (currentEvent.event) { + case "progress": + handlers.onProgress?.(parsedData.message); + break; + case "result": + result = parsedData; + handlers.onResult?.(parsedData); + break; + case "error": + handlers.onError?.(parsedData.error); + throw new Error(parsedData.error); + } + } + + // Reset for next event + currentEvent = {}; + continue; + } + + // Parse SSE field + const colonIndex = line.indexOf(":"); + if (colonIndex === -1) { + continue; + } + + const field = line.slice(0, colonIndex); + let value = line.slice(colonIndex + 1); + + // Trim leading space from value (SSE spec) + if (value.startsWith(" ")) { + value = value.slice(1); + } + + switch (field) { + case "event": + currentEvent.event = value; + break; + case "data": + currentEvent.data = value; + break; + case "id": + currentEvent.id = value; + break; + } + } + } + + return result; + } finally { + reader.releaseLock(); + } +}