Skip to content

Instantly share code, notes, and snippets.

@dansimau
Created September 26, 2023 14:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dansimau/6657920a51c83212ebff929f85b5d309 to your computer and use it in GitHub Desktop.
Save dansimau/6657920a51c83212ebff929f85b5d309 to your computer and use it in GitHub Desktop.
Command execution in Maestro

Quick start

  • Start the exec server
tsx exec-server.ts &
  • Run Maestro test
maestro test ./example-flow.yaml
  • Profit

How it works

  • A server runs on 127.0.0.1:4567 and listens on the /exec endpoint for commands to run
  • Commands are send from Maestro via the HTTP client to the exec server
  • The server returns the following:
    • exitCode
    • stdout
    • stderr
  • Pass parseJson: true to parse stdout as JSON and make it available in the json field
  • Specify a resultId so you can access the results of previous commands as variables, e.g. ${output.yourResultId.stdout}
appId: ''
---
# Fetch products
- runScript:
file: exec.js
env:
command: 'curl -v https://dummyjson.com/products'
resultId: productsQuery
parseJson: true
# Log curl result
- evalScript: ${console.log(output.productsQuery.stdout)}
# Fetch first product returned, by ID
- runScript:
file: exec.js
env:
command: 'curl -v https://dummyjson.com/products/${output.productsQuery.json.products[0].id}'
resultId: firstProductQuery
parseJson: true
# Log product title
- evalScript: ${console.log(output.firstProductQuery.json.title)}
#!/usr/bin/env tsx
import process from 'child_process';
import express from 'express';
import { Buffer } from 'node:buffer';
const host = '127.0.0.1';
const port = 4567;
const app = express();
app.use(express.json());
app.listen(port, host, () => {
console.log(`Listening on ${host}:${port}`);
});
app.post('/exec', (req, res) => {
const args = req.body.args;
if (!isStringArray(args)) {
console.error(`[${new Date()}]: Invalid args: ${req.body.args}`);
res.status(400).send({
error: 'invalid args',
});
return;
}
console.log(`[${new Date()}]: Exec: ${args.join(', ')}`);
const proc = process.spawn(args[0], args.slice(1), {
shell: true,
});
const stdout: Buffer[] = [];
const stderr: Buffer[] = [];
proc.stdout.on('data', (data) => stdout.push(data));
proc.stderr.on('data', (data) => stderr.push(data));
proc.on('error', (err) => {
console.error(`[${new Date()}]: Failed to spawn: ${err}`);
res.status(500).send({
exitCode: 127,
stdout: '',
stderr: err,
});
});
proc.on('close', (exitCode) => {
if (!res.headersSent) {
const responseStatus = exitCode === 0 ? 200 : 500;
res.status(responseStatus).send({
exitCode,
stdout: Buffer.concat(stdout).toString('utf-8'),
stderr: Buffer.concat(stderr).toString('utf-8'),
});
}
});
});
function isStringArray(v: string[] | unknown): v is string[] {
if (!Array.isArray(v)) {
return false;
}
return v.every((i) => typeof i === 'string');
}
if (typeof command === 'undefined') {
throw new Error('missing command');
}
const response = http.post('http://127.0.0.1:4567/exec', {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
args: [command],
}),
});
if (!response.ok) {
throw new Error(`command failed: ${response.body}`);
}
const parsed = json(response.body);
const result = {
stdout: parsed.stdout,
stderr: parsed.stderr,
exitCode: parsed.exitCode,
};
if (parseJson) {
result.json = json(parsed.stdout);
}
output.exec = result;
if (typeof resultId !== 'undefined') {
output[resultId] = result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment