Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/clear-pears-know.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@plotday/sdk": patch
---

Added: Progress updates for agent generate and deploy
5 changes: 5 additions & 0 deletions .changeset/long-baths-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@plotday/sdk": patch
---

Added: Install latest SDK package after generate
24 changes: 1 addition & 23 deletions sdk/cli/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,14 @@ 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;
name?: string;
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");

Expand Down
9 changes: 8 additions & 1 deletion sdk/cli/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand All @@ -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) {
Expand Down
46 changes: 45 additions & 1 deletion sdk/cli/commands/generate.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 }),
Expand All @@ -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)) {
Expand Down Expand Up @@ -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!");

Expand Down
25 changes: 25 additions & 0 deletions sdk/cli/utils/packageManager.ts
Original file line number Diff line number Diff line change
@@ -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";
}
116 changes: 116 additions & 0 deletions sdk/cli/utils/sse.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
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<SSEEvent> = {};

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();
}
}