feat: add execute command for function execution with streaming options
This commit is contained in:
@@ -33,6 +33,8 @@ this will check the health, and if not setup, it will prompt you to set up the C
|
||||
- `shsf get namespace <id>`: Get details of a specific namespace by its ID
|
||||
- `shsf get trigger <functionId> <triggerId>`: Get details of a specific trigger from a function.
|
||||
|
||||
- `shsf execute --id <id> [--payload <json>] [--no-stream]`: Execute a function and stream the output (debug/internal).
|
||||
|
||||
- `shsf storage create --name <name> --purpose <purpose>`: Create a new storage.
|
||||
- `shsf storage delete --name <name>`: Delete a storage.
|
||||
- `shsf storage list`: List all storages.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "shsf-cli",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"description": "",
|
||||
"type": "module",
|
||||
"files": [
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import chalk from "chalk";
|
||||
import { getApiClient } from "../../api.js";
|
||||
import { AxiosResponse } from "axios";
|
||||
|
||||
export const functionExecuteDefinition = {
|
||||
name: "execute",
|
||||
description: "Execute a function and stream the output (debug/internal).",
|
||||
options: [
|
||||
{ name: "--id <id>", description: "Function ID", required: true },
|
||||
{ name: "--payload <json>", description: "JSON payload for execution", required: false },
|
||||
{ name: "--no-stream", description: "Disable streaming and get final result only", required: false, default: false },
|
||||
],
|
||||
action: async (options: any) => {
|
||||
const client = await getApiClient();
|
||||
|
||||
// Explicitly check for noStream from commander
|
||||
// Commander uses camelsCase for options, so --no-stream becomes stream: false
|
||||
// but we use --no-stream which commander natively handles as a boolean toggle
|
||||
const useStream = options.stream !== false;
|
||||
|
||||
let payload = {};
|
||||
if (options.payload) {
|
||||
try {
|
||||
payload = JSON.parse(options.payload);
|
||||
} catch (error: any) {
|
||||
console.error(`${chalk.red("✗")} Invalid JSON payload: ${chalk.yellow(error.message)}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const handleJsonChunk = (chunk: string) => {
|
||||
const trimmed = chunk.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
try {
|
||||
const obj = JSON.parse(trimmed);
|
||||
if (obj.type === "output" && obj.content) {
|
||||
process.stdout.write(obj.content);
|
||||
} else if (obj.type === "end") {
|
||||
if (obj.exitCode !== 0 && obj.exitCode !== undefined) {
|
||||
console.error(`\n${chalk.red("✗")} Execution failed with exit code ${obj.exitCode}`);
|
||||
} else {
|
||||
console.log(`\n${chalk.green("✓")} Execution completed.`);
|
||||
}
|
||||
if (obj.result !== undefined) {
|
||||
console.log(chalk.blue("Result:"), typeof obj.result === 'object' ? JSON.stringify(obj.result, null, 2) : obj.result);
|
||||
}
|
||||
if (obj.took) {
|
||||
const total = obj.took.find((t: any) => t.description === "Total");
|
||||
if (total) {
|
||||
console.log(chalk.gray(`Total time: ${total.value}s`));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// If it's not JSON, it might be raw output from a non-stream response
|
||||
// that got into the stream handler mistakenly or just raw noise
|
||||
if (trimmed.length > 0) {
|
||||
process.stdout.write(chunk);
|
||||
}
|
||||
// Not a full JSON object yet or invalid JSON
|
||||
}
|
||||
};
|
||||
|
||||
console.log(`${chalk.blue("ℹ")} Executing function ${chalk.cyan(options.id)} (stream: ${useStream})...`);
|
||||
|
||||
try {
|
||||
const response = await client.post(`/api/function/${options.id}/execute`,
|
||||
{ run: payload },
|
||||
{
|
||||
params: { stream: useStream ? "true" : "false" },
|
||||
responseType: useStream ? "stream" : "json"
|
||||
}
|
||||
);
|
||||
|
||||
if (useStream) {
|
||||
const stream = (response as AxiosResponse).data;
|
||||
let buffer = "";
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
stream.on("data", (chunk: any) => {
|
||||
const str = chunk.toString();
|
||||
buffer += str;
|
||||
|
||||
// Try to split by potential JSON boundaries
|
||||
// Matches cases like ...}{...
|
||||
const pattern = /\}\s*\{/g;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
|
||||
while ((match = pattern.exec(buffer)) !== null) {
|
||||
const part = buffer.slice(lastIndex, match.index + 1);
|
||||
handleJsonChunk(part);
|
||||
lastIndex = match.index + match[0].indexOf('{');
|
||||
}
|
||||
|
||||
buffer = buffer.slice(lastIndex);
|
||||
|
||||
// Attempt to parse the remaining buffer if it looks like a complete object
|
||||
try {
|
||||
if (buffer.trim().startsWith('{') && buffer.trim().endsWith('}')) {
|
||||
handleJsonChunk(buffer);
|
||||
buffer = "";
|
||||
}
|
||||
} catch (e) {
|
||||
// Wait for more data
|
||||
}
|
||||
});
|
||||
|
||||
stream.on("end", () => {
|
||||
if (buffer.trim()) {
|
||||
handleJsonChunk(buffer);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
stream.on("error", (err: Error) => {
|
||||
console.error(`\n${chalk.red("✗")} Stream error: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Non-stream returns just the output content or the body
|
||||
const result = response.data;
|
||||
if (typeof result === 'string') {
|
||||
process.stdout.write(result);
|
||||
} else if (result && result.data && typeof result.data === 'string') {
|
||||
process.stdout.write(result.data);
|
||||
} else if (result && typeof result.output === 'string') {
|
||||
process.stdout.write(result.output);
|
||||
} else if (result && typeof result.result === 'string') {
|
||||
process.stdout.write(result.result);
|
||||
} else if (result && result.data && result.data.output) {
|
||||
process.stdout.write(result.data.output);
|
||||
} else {
|
||||
process.stdout.write(JSON.stringify(result, null, 2));
|
||||
}
|
||||
console.log(`\n${chalk.green("✓")} Execution completed.`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.response) {
|
||||
console.error(
|
||||
`${chalk.red("✗")} Execution failed: ${chalk.yellow(error.response.data?.message || error.response.statusText || "Unknown error")}`,
|
||||
);
|
||||
} else if (error.request) {
|
||||
console.error(`${chalk.red("✗")} No response received from server.`);
|
||||
} else {
|
||||
console.error(`${chalk.red("✗")} Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user