Skip to content

Commit 374f952

Browse files
authored
Merge pull request #25 from plotday/feat/streaming-updates
Generate and deploy progress updates; install SDK after generate
2 parents b77fe24 + b9c3528 commit 374f952

7 files changed

Lines changed: 205 additions & 25 deletions

File tree

.changeset/clear-pears-know.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@plotday/sdk": patch
3+
---
4+
5+
Added: Progress updates for agent generate and deploy

.changeset/long-baths-invent.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@plotday/sdk": patch
3+
---
4+
5+
Added: Install latest SDK package after generate

sdk/cli/commands/create.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,14 @@ import * as fs from "fs";
33
import * as path from "path";
44
import prompts from "prompts";
55
import * as out from "../utils/output";
6+
import { detectPackageManager } from "../utils/packageManager";
67

78
interface CreateOptions {
89
dir?: string;
910
name?: string;
1011
displayName?: string;
1112
}
1213

13-
/**
14-
* Detects the package manager being used
15-
* Checks for lock files and npm_config_user_agent
16-
*/
17-
function detectPackageManager(): string {
18-
// Check npm_config_user_agent first (set by npm, yarn, pnpm)
19-
const userAgent = process.env.npm_config_user_agent;
20-
if (userAgent) {
21-
if (userAgent.includes("yarn")) return "yarn";
22-
if (userAgent.includes("pnpm")) return "pnpm";
23-
if (userAgent.includes("npm")) return "npm";
24-
}
25-
26-
// Check for lock files in current directory
27-
const cwd = process.cwd();
28-
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
29-
if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
30-
if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm";
31-
32-
// Default to npm
33-
return "npm";
34-
}
35-
3614
export async function createCommand(options: CreateOptions) {
3715
out.header("Create a new Plot agent");
3816

sdk/cli/commands/deploy.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import prompts from "prompts";
66
import * as out from "../utils/output";
77
import { getGlobalTokenPath } from "../utils/token";
88
import { bundleAgent } from "../utils/bundle";
9+
import { handleSSEStream } from "../utils/sse";
910

1011
interface DeployOptions {
1112
dir: string;
@@ -257,6 +258,7 @@ export async function deployCommand(options: DeployOptions) {
257258
method: "POST",
258259
headers: {
259260
"Content-Type": "application/json",
261+
Accept: "text/event-stream",
260262
Authorization: `Bearer ${deployToken}`,
261263
},
262264
body: JSON.stringify(requestBody),
@@ -271,7 +273,12 @@ export async function deployCommand(options: DeployOptions) {
271273
process.exit(1);
272274
}
273275

274-
const result = (await response.json()) as any;
276+
// Handle SSE stream with progress updates
277+
const result = (await handleSSEStream(response, {
278+
onProgress: (message) => {
279+
out.progress(message);
280+
},
281+
})) as any;
275282

276283
// Handle dryRun response
277284
if (options.dryRun) {

sdk/cli/commands/generate.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { execSync } from "child_process";
12
import * as dotenv from "dotenv";
23
import * as fs from "fs";
34
import * as path from "path";
45
import prompts from "prompts";
56

67
import * as out from "../utils/output";
8+
import { detectPackageManager } from "../utils/packageManager";
79
import { getGlobalTokenPath } from "../utils/token";
10+
import { handleSSEStream } from "../utils/sse";
811

912
interface GenerateOptions {
1013
dir: string;
@@ -175,6 +178,7 @@ export async function generateCommand(options: GenerateOptions) {
175178
method: "POST",
176179
headers: {
177180
"Content-Type": "application/json",
181+
Accept: "text/event-stream",
178182
Authorization: `Bearer ${deployToken}`,
179183
},
180184
body: JSON.stringify({ spec: specContent }),
@@ -189,7 +193,12 @@ export async function generateCommand(options: GenerateOptions) {
189193
process.exit(1);
190194
}
191195

192-
const source = (await response.json()) as AgentSource;
196+
// Handle SSE stream with progress updates
197+
const source = (await handleSSEStream(response, {
198+
onProgress: (message) => {
199+
out.progress(message);
200+
},
201+
})) as AgentSource;
193202

194203
// Create agent directory if it doesn't exist
195204
if (!fs.existsSync(agentPath)) {
@@ -295,6 +304,41 @@ export async function generateCommand(options: GenerateOptions) {
295304
writeFile(filePath, content);
296305
}
297306

307+
out.blank();
308+
309+
// Detect package manager and install dependencies
310+
const packageManager = detectPackageManager();
311+
312+
// Update @plotday/sdk to latest and install packages
313+
try {
314+
out.progress("Updating @plotday/sdk to latest version...");
315+
316+
const updateCommand =
317+
packageManager === "npm" ? "npm install @plotday/sdk@latest" :
318+
packageManager === "pnpm" ? "pnpm add @plotday/sdk@latest" :
319+
"yarn add @plotday/sdk@latest";
320+
321+
execSync(updateCommand, { cwd: agentPath, stdio: "ignore" });
322+
323+
out.progress("Installing dependencies...");
324+
325+
const installCommand =
326+
packageManager === "yarn" ? "yarn" :
327+
`${packageManager} install`;
328+
329+
execSync(installCommand, { cwd: agentPath, stdio: "ignore" });
330+
331+
out.success("Dependencies installed successfully!");
332+
} catch (error) {
333+
out.warning(
334+
"Couldn't install dependencies",
335+
[
336+
`Run '${packageManager === "npm" ? "npm install @plotday/sdk@latest" : packageManager === "pnpm" ? "pnpm add @plotday/sdk@latest" : "yarn add @plotday/sdk@latest"}' in ${options.dir}`,
337+
`Then run '${packageManager === "yarn" ? "yarn" : `${packageManager} install`}'`
338+
]
339+
);
340+
}
341+
298342
out.blank();
299343
out.success("Agent generated successfully!");
300344

sdk/cli/utils/packageManager.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
4+
/**
5+
* Detects the package manager being used
6+
* Checks for npm_config_user_agent and lock files
7+
*/
8+
export function detectPackageManager(): string {
9+
// Check npm_config_user_agent first (set by npm, yarn, pnpm)
10+
const userAgent = process.env.npm_config_user_agent;
11+
if (userAgent) {
12+
if (userAgent.includes("yarn")) return "yarn";
13+
if (userAgent.includes("pnpm")) return "pnpm";
14+
if (userAgent.includes("npm")) return "npm";
15+
}
16+
17+
// Check for lock files in current directory
18+
const cwd = process.cwd();
19+
if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
20+
if (fs.existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
21+
if (fs.existsSync(path.join(cwd, "package-lock.json"))) return "npm";
22+
23+
// Default to npm
24+
return "npm";
25+
}

sdk/cli/utils/sse.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* Server-Sent Events (SSE) client utilities
3+
*/
4+
5+
export interface SSEEvent {
6+
event: string;
7+
data: any;
8+
id?: string;
9+
}
10+
11+
export interface SSEHandlers {
12+
onProgress?: (message: string) => void;
13+
onResult?: (data: any) => void;
14+
onError?: (error: string) => void;
15+
}
16+
17+
/**
18+
* Parse and handle SSE response stream
19+
*/
20+
export async function handleSSEStream(
21+
response: Response,
22+
handlers: SSEHandlers
23+
): Promise<any> {
24+
if (!response.body) {
25+
throw new Error("Response has no body");
26+
}
27+
28+
const reader = response.body.getReader();
29+
const decoder = new TextDecoder();
30+
let buffer = "";
31+
let result: any = null;
32+
33+
try {
34+
while (true) {
35+
const { done, value } = await reader.read();
36+
37+
if (done) {
38+
break;
39+
}
40+
41+
// Decode chunk and add to buffer
42+
buffer += decoder.decode(value, { stream: true });
43+
44+
// Process complete messages in buffer
45+
const lines = buffer.split("\n");
46+
47+
// Keep incomplete message in buffer
48+
buffer = lines.pop() || "";
49+
50+
let currentEvent: Partial<SSEEvent> = {};
51+
52+
for (const line of lines) {
53+
// Empty line marks end of event
54+
if (line.trim() === "") {
55+
if (currentEvent.event && currentEvent.data !== undefined) {
56+
// Parse data if it's JSON
57+
let parsedData = currentEvent.data;
58+
try {
59+
parsedData = JSON.parse(currentEvent.data);
60+
} catch {
61+
// Not JSON, use as-is
62+
}
63+
64+
// Handle event based on type
65+
switch (currentEvent.event) {
66+
case "progress":
67+
handlers.onProgress?.(parsedData.message);
68+
break;
69+
case "result":
70+
result = parsedData;
71+
handlers.onResult?.(parsedData);
72+
break;
73+
case "error":
74+
handlers.onError?.(parsedData.error);
75+
throw new Error(parsedData.error);
76+
}
77+
}
78+
79+
// Reset for next event
80+
currentEvent = {};
81+
continue;
82+
}
83+
84+
// Parse SSE field
85+
const colonIndex = line.indexOf(":");
86+
if (colonIndex === -1) {
87+
continue;
88+
}
89+
90+
const field = line.slice(0, colonIndex);
91+
let value = line.slice(colonIndex + 1);
92+
93+
// Trim leading space from value (SSE spec)
94+
if (value.startsWith(" ")) {
95+
value = value.slice(1);
96+
}
97+
98+
switch (field) {
99+
case "event":
100+
currentEvent.event = value;
101+
break;
102+
case "data":
103+
currentEvent.data = value;
104+
break;
105+
case "id":
106+
currentEvent.id = value;
107+
break;
108+
}
109+
}
110+
}
111+
112+
return result;
113+
} finally {
114+
reader.releaseLock();
115+
}
116+
}

0 commit comments

Comments
 (0)