feat(executors): add Railway executor template and documentation
- Add Railway executor template with one-click deploy support - Create Railway documentation page at /docs/executors/railway - Update main executors page with Railway as official recommendation - Add Railway to platform comparison table with new columns - Update inter-page navigation for Railway → Unsandbox → Vercel flow - Update FAQ to recommend Railway for most use cases Railway executor features: - Zero-dependency Node.js HTTP server - Docker support via included Dockerfile - Health check endpoint at /health - Tool execution at /execute-tool - API key authentication support - Auto-restart on failure via railway.json
This commit is contained in:
parent
3f62228c56
commit
ffc6ddcdbb
9 changed files with 1251 additions and 19 deletions
|
|
@ -119,7 +119,54 @@ export default function ExecutorsDocsPage(): React.ReactElement {
|
|||
needs:
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{/* Railway Card */}
|
||||
<Link
|
||||
href="/docs/executors/railway"
|
||||
className="group p-6 bg-surface border border-border rounded-lg hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-12 h-12 bg-[#0B0D0E] rounded-lg flex items-center justify-center">
|
||||
<svg
|
||||
className="w-7 h-7"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="img"
|
||||
aria-labelledby="railway-logo-title"
|
||||
>
|
||||
<title id="railway-logo-title">Railway logo</title>
|
||||
<path
|
||||
d="M.113 12.611c-.139.312-.107.633.092.917.156.222.38.35.651.357h5.523a.28.28 0 00.216-.1c.07-.078.135-.22.2-.426.224-.718.428-1.09.65-1.492l.003-.005c.247-.448.51-.924.817-1.762.26-.712.296-1.371.113-2.008-.178-.62-.627-1.213-1.406-1.823a.248.248 0 00-.23-.044c-.078.025-.14.08-.178.157l-5.46 4.67a1.017 1.017 0 00-.287.41z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-foreground group-hover:text-primary transition-colors">
|
||||
Railway
|
||||
</h3>
|
||||
<p className="text-sm text-foreground-secondary mt-1">
|
||||
Always-on with auto-scaling and a $5/month free tier. One-click deploy.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
<span className="px-2 py-0.5 text-xs bg-success/10 text-success rounded">
|
||||
Official
|
||||
</span>
|
||||
<span className="px-2 py-0.5 text-xs bg-surface-secondary rounded text-foreground-tertiary">
|
||||
Free tier
|
||||
</span>
|
||||
<span className="px-2 py-0.5 text-xs bg-surface-secondary rounded text-foreground-tertiary">
|
||||
Auto-scaling
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Icon
|
||||
icon="chevronRight"
|
||||
className="w-5 h-5 text-foreground-tertiary group-hover:text-primary transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{/* Unsandbox Card */}
|
||||
<Link
|
||||
href="/docs/executors/unsandbox"
|
||||
|
|
@ -138,9 +185,6 @@ export default function ExecutorsDocsPage(): React.ReactElement {
|
|||
command.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
<span className="px-2 py-0.5 text-xs bg-success/10 text-success rounded">
|
||||
Recommended
|
||||
</span>
|
||||
<span className="px-2 py-0.5 text-xs bg-surface-secondary rounded text-foreground-tertiary">
|
||||
No cold starts
|
||||
</span>
|
||||
|
|
@ -167,8 +211,10 @@ export default function ExecutorsDocsPage(): React.ReactElement {
|
|||
className="w-6 h-6 text-white"
|
||||
viewBox="0 0 76 65"
|
||||
fill="currentColor"
|
||||
aria-label="Vercel logo"
|
||||
role="img"
|
||||
aria-labelledby="vercel-logo-title"
|
||||
>
|
||||
<title id="vercel-logo-title">Vercel logo</title>
|
||||
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
|
@ -177,15 +223,14 @@ export default function ExecutorsDocsPage(): React.ReactElement {
|
|||
Vercel
|
||||
</h3>
|
||||
<p className="text-sm text-foreground-secondary mt-1">
|
||||
Serverless execution with VM-level isolation using Vercel Sandbox. One-click
|
||||
deploy.
|
||||
Serverless execution with VM-level isolation using Vercel Sandbox.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
<span className="px-2 py-0.5 text-xs bg-surface-secondary rounded text-foreground-tertiary">
|
||||
One-click deploy
|
||||
</span>
|
||||
<span className="px-2 py-0.5 text-xs bg-surface-secondary rounded text-foreground-tertiary">
|
||||
Free tier available
|
||||
Pay-per-use
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -211,6 +256,7 @@ export default function ExecutorsDocsPage(): React.ReactElement {
|
|||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="text-left py-3 pr-4 font-medium text-foreground">Feature</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-foreground">Railway</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-foreground">Unsandbox</th>
|
||||
<th className="text-left py-3 pl-4 font-medium text-foreground">Vercel</th>
|
||||
</tr>
|
||||
|
|
@ -218,12 +264,14 @@ export default function ExecutorsDocsPage(): React.ReactElement {
|
|||
<tbody className="text-foreground-secondary">
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 pr-4">Deploy method</td>
|
||||
<td className="py-3 px-4">One-click / CLI</td>
|
||||
<td className="py-3 px-4">CLI command</td>
|
||||
<td className="py-3 pl-4">One-click button</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 pr-4">Isolation</td>
|
||||
<td className="py-3 px-4">Container-level</td>
|
||||
<td className="py-3 px-4">Container-level</td>
|
||||
<td className="py-3 pl-4">VM-level (Sandbox)</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/50">
|
||||
|
|
@ -231,6 +279,9 @@ export default function ExecutorsDocsPage(): React.ReactElement {
|
|||
<td className="py-3 px-4">
|
||||
<span className="text-success">None (always-on)</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-success">None (always-on)</span>
|
||||
</td>
|
||||
<td className="py-3 pl-4">Yes (serverless)</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/50">
|
||||
|
|
@ -238,22 +289,44 @@ export default function ExecutorsDocsPage(): React.ReactElement {
|
|||
<td className="py-3 px-4">
|
||||
<span className="text-success">Unlimited</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-success">Unlimited</span>
|
||||
</td>
|
||||
<td className="py-3 pl-4">45min (Hobby) / 5hr (Pro)</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 pr-4">Free tier</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-success">$5/month credit</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">None</td>
|
||||
<td className="py-3 pl-4">Limited</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 pr-4">Pricing</td>
|
||||
<td className="py-3 px-4">Per usage</td>
|
||||
<td className="py-3 px-4">Per uptime</td>
|
||||
<td className="py-3 pl-4">Per compute time</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 pr-4">Auto-scaling</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="text-success">Yes</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">Manual</td>
|
||||
<td className="py-3 pl-4">Yes</td>
|
||||
</tr>
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 pr-4">Custom domains</td>
|
||||
<td className="py-3 px-4">Yes</td>
|
||||
<td className="py-3 px-4">Yes</td>
|
||||
<td className="py-3 pl-4">Yes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 pr-4">Freeze/unfreeze</td>
|
||||
<td className="py-3 px-4">Yes (save costs)</td>
|
||||
<td className="py-3 pl-4">N/A (serverless)</td>
|
||||
<td className="py-3 pr-4">Docker support</td>
|
||||
<td className="py-3 px-4">Yes</td>
|
||||
<td className="py-3 px-4">Yes</td>
|
||||
<td className="py-3 pl-4">No</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -387,18 +460,18 @@ export default function ExecutorsDocsPage(): React.ReactElement {
|
|||
Which platform should I choose?
|
||||
</h3>
|
||||
<p className="text-foreground-secondary text-sm">
|
||||
<strong>Unsandbox</strong> is recommended for most use cases. It has no cold
|
||||
starts, unlimited runtime, and simple CLI deployment. Use <strong>Vercel</strong>{' '}
|
||||
if you're already on Vercel or prefer one-click deployment and pay-per-use
|
||||
pricing.
|
||||
<strong>Railway</strong> is our official recommendation. It offers one-click
|
||||
deployment, no cold starts, auto-scaling, and a generous $5/month free tier. Use{' '}
|
||||
<strong>Unsandbox</strong> if you prefer CLI deployment, or{' '}
|
||||
<strong>Vercel</strong> if you're already on Vercel and prefer pay-per-use
|
||||
serverless pricing.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium text-foreground mb-2">Can I use other platforms?</h3>
|
||||
<p className="text-foreground-secondary text-sm">
|
||||
Yes! Any platform that runs Node.js and exposes HTTP endpoints works. AWS Lambda,
|
||||
Google Cloud Run, Railway, Render, Fly.io—just implement the API specification
|
||||
above.
|
||||
Google Cloud Run, Render, Fly.io—just implement the API specification above.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
442
apps/web/src/app/docs/executors/railway/page.tsx
Normal file
442
apps/web/src/app/docs/executors/railway/page.tsx
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { CodeBlock } from '@tpmjs/ui/CodeBlock/CodeBlock';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { AppFooter } from '~/components/AppFooter';
|
||||
import { AppHeader } from '~/components/AppHeader';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Deploy to Railway - Custom Executors - TPMJS',
|
||||
description:
|
||||
'Deploy a TPMJS executor to Railway with one click. Always-on, auto-scaling, with a generous free tier.',
|
||||
};
|
||||
|
||||
const healthCheck = `curl https://your-executor.up.railway.app/health`;
|
||||
|
||||
const healthResponse = `{
|
||||
"status": "ok",
|
||||
"version": "1.0.0",
|
||||
"info": {
|
||||
"runtime": "railway",
|
||||
"timestamp": "2024-01-01T00:00:00.000Z",
|
||||
"region": "us-west1"
|
||||
}
|
||||
}`;
|
||||
|
||||
const executeExample = `curl -X POST https://your-executor.up.railway.app/execute-tool \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "Authorization: Bearer your-api-key" \\
|
||||
-d '{
|
||||
"packageName": "@tpmjs/hello",
|
||||
"name": "helloWorldTool",
|
||||
"version": "latest",
|
||||
"params": { "includeTimestamp": true }
|
||||
}'`;
|
||||
|
||||
const cliDeploy = `# Clone the template
|
||||
git clone https://github.com/tpmjs/tpmjs.git
|
||||
cd tpmjs/templates/railway-executor
|
||||
|
||||
# Install Railway CLI
|
||||
npm install -g @railway/cli
|
||||
|
||||
# Login to Railway
|
||||
railway login
|
||||
|
||||
# Create a new project and deploy
|
||||
railway init
|
||||
railway up`;
|
||||
|
||||
const envVars = `# Set environment variables via CLI
|
||||
railway variables set EXECUTOR_API_KEY=your-secure-key
|
||||
railway variables set OPENAI_API_KEY=sk-xxx
|
||||
railway variables set DATABASE_URL=postgres://...`;
|
||||
|
||||
const localDev = `# Clone the repository
|
||||
git clone https://github.com/tpmjs/tpmjs.git
|
||||
cd tpmjs/templates/railway-executor
|
||||
|
||||
# Run locally
|
||||
PORT=3000 node index.js
|
||||
|
||||
# Test health endpoint
|
||||
curl http://localhost:3000/health`;
|
||||
|
||||
const dockerDeploy = `# Build the image
|
||||
docker build -t tpmjs-executor .
|
||||
|
||||
# Run locally
|
||||
docker run -p 3000:3000 -e EXECUTOR_API_KEY=your-key tpmjs-executor`;
|
||||
|
||||
export default function RailwayExecutorPage(): React.ReactElement {
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
<AppHeader />
|
||||
|
||||
<main className="flex-1">
|
||||
<div className="max-w-4xl mx-auto px-4 py-12">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center gap-2 text-sm text-foreground-secondary mb-8">
|
||||
<Link href="/docs/executors" className="hover:text-foreground transition-colors">
|
||||
Executors
|
||||
</Link>
|
||||
<Icon icon="chevronRight" className="w-4 h-4" />
|
||||
<span className="text-foreground">Railway</span>
|
||||
</nav>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-12">
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="w-12 h-12 bg-[#0B0D0E] rounded-lg flex items-center justify-center">
|
||||
<svg
|
||||
className="w-7 h-7"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="img"
|
||||
aria-labelledby="railway-header-logo"
|
||||
>
|
||||
<title id="railway-header-logo">Railway logo</title>
|
||||
<path
|
||||
d="M.113 12.611c-.139.312-.107.633.092.917.156.222.38.35.651.357h5.523a.28.28 0 00.216-.1c.07-.078.135-.22.2-.426.224-.718.428-1.09.65-1.492l.003-.005c.247-.448.51-.924.817-1.762.26-.712.296-1.371.113-2.008-.178-.62-.627-1.213-1.406-1.823a.248.248 0 00-.23-.044c-.078.025-.14.08-.178.157l-5.46 4.67a1.017 1.017 0 00-.287.41z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M8.904 6.16a.258.258 0 00-.22.108c-.06.08-.08.178-.053.27.147.517.147 1.023 0 1.56-.114.416-.304.81-.49 1.194l-.04.083c-.214.443-.43.889-.595 1.42-.204.66-.21 1.336-.017 2.015.287 1.013.986 1.826 1.93 2.252l.014.006c.15.07.252.116.341.196.09.08.17.195.28.368l.023.035c.217.34.517.81.827 1.142a.244.244 0 00.204.091h1.694a.252.252 0 00.217-.12.234.234 0 00.006-.243 7.639 7.639 0 00-.66-1.016c-.253-.34-.523-.665-.76-.948a6.876 6.876 0 01-.507-.66c-.15-.228-.288-.53-.296-.857-.016-.679.413-1.39.85-1.994.168-.232.35-.484.52-.767.306-.51.383-1.054.227-1.62a2.568 2.568 0 00-.943-1.317 6.487 6.487 0 00-.98-.636 2.27 2.27 0 01-.516-.346.248.248 0 00-.214-.073l-.842.057z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M20.756 6.225h-6.243a.255.255 0 00-.23.146.243.243 0 00.035.266c.32.362.618.742.892 1.137.317.46.597.946.833 1.45a.243.243 0 00.218.142h5.808c.278-.005.507-.133.667-.36.203-.287.234-.62.091-.94l-.765-1.563a.264.264 0 00-.24-.152l-1.066-.126z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M15.82 11.152a.247.247 0 00-.229.158c-.243.648-.36.972-.618 1.46-.257.49-.527.84-1.058 1.468a.244.244 0 00.006.325c.235.264.496.505.78.722.425.327.883.608 1.365.837a.237.237 0 00.108.026h4.16a.255.255 0 00.229-.146.243.243 0 00-.035-.267c-.444-.502-.8-.98-1.172-1.632l-.006-.01c-.367-.642-.57-1.092-.83-1.872a.243.243 0 00-.232-.169l-2.468-.9z"
|
||||
fill="#fff"
|
||||
/>
|
||||
<path
|
||||
d="M13.873 16.68a.25.25 0 00-.2.074 4.588 4.588 0 01-.637.52c-.456.314-.797.472-1.236.647a.24.24 0 00-.147.137.249.249 0 00.004.2l.463.95c.098.2.285.32.5.317h6.357c.273-.013.497-.144.653-.374.198-.29.223-.622.074-.935l-.553-1.131a.257.257 0 00-.236-.152l-5.042-.253z"
|
||||
fill="#fff"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-foreground">Deploy to Railway</h1>
|
||||
<p className="text-foreground-secondary">
|
||||
Always-on execution with auto-scaling and a free tier
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Why Railway */}
|
||||
<section className="mb-12">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
<div className="p-4 bg-surface border border-border rounded-lg">
|
||||
<div className="text-2xl mb-2">$5</div>
|
||||
<div className="text-sm text-foreground-secondary">Free monthly credit</div>
|
||||
</div>
|
||||
<div className="p-4 bg-surface border border-border rounded-lg">
|
||||
<div className="text-2xl mb-2">0ms</div>
|
||||
<div className="text-sm text-foreground-secondary">No cold starts</div>
|
||||
</div>
|
||||
<div className="p-4 bg-surface border border-border rounded-lg">
|
||||
<div className="text-2xl mb-2">Auto</div>
|
||||
<div className="text-sm text-foreground-secondary">Scaling built-in</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* One-Click Deploy */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">One-Click Deploy</h2>
|
||||
<p className="text-foreground-secondary mb-6">
|
||||
Deploy the TPMJS executor to Railway with a single click:
|
||||
</p>
|
||||
<a
|
||||
href="https://railway.app/template/tpmjs-executor?referralCode=tpmjs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button size="lg">
|
||||
<svg
|
||||
className="w-4 h-4 mr-2"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M.113 12.611c-.139.312-.107.633.092.917.156.222.38.35.651.357h5.523a.28.28 0 00.216-.1c.07-.078.135-.22.2-.426.224-.718.428-1.09.65-1.492l.003-.005c.247-.448.51-.924.817-1.762.26-.712.296-1.371.113-2.008-.178-.62-.627-1.213-1.406-1.823a.248.248 0 00-.23-.044c-.078.025-.14.08-.178.157l-5.46 4.67a1.017 1.017 0 00-.287.41z" />
|
||||
</svg>
|
||||
Deploy on Railway
|
||||
</Button>
|
||||
</a>
|
||||
<p className="text-sm text-foreground-tertiary mt-4">
|
||||
After deployment, your executor will be available at{' '}
|
||||
<code className="px-1.5 py-0.5 bg-surface rounded">
|
||||
https://your-project.up.railway.app
|
||||
</code>
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* CLI Deploy */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">Deploy via CLI</h2>
|
||||
<p className="text-foreground-secondary mb-4">
|
||||
Prefer the command line? Deploy with the Railway CLI:
|
||||
</p>
|
||||
<CodeBlock language="bash" code={cliDeploy} />
|
||||
</section>
|
||||
|
||||
{/* Test Your Deployment */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">Test Your Deployment</h2>
|
||||
<p className="text-foreground-secondary mb-4">Verify your executor is running:</p>
|
||||
<CodeBlock language="bash" code={healthCheck} />
|
||||
<p className="text-sm text-foreground-secondary mt-4 mb-2">Expected response:</p>
|
||||
<CodeBlock language="json" code={healthResponse} />
|
||||
</section>
|
||||
|
||||
{/* Authentication */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">Add Authentication</h2>
|
||||
<div className="p-4 bg-warning/10 border border-warning/30 rounded-lg mb-4">
|
||||
<p className="text-sm text-warning">
|
||||
<strong>Important:</strong> Without an API key, anyone can execute tools on your
|
||||
executor. Always set{' '}
|
||||
<code className="px-1 bg-warning/20 rounded">EXECUTOR_API_KEY</code> in production.
|
||||
</p>
|
||||
</div>
|
||||
<ol className="text-foreground-secondary space-y-3">
|
||||
<li className="flex gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-surface-secondary text-foreground-secondary text-sm flex items-center justify-center">
|
||||
1
|
||||
</span>
|
||||
<span>Go to your Railway project dashboard</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-surface-secondary text-foreground-secondary text-sm flex items-center justify-center">
|
||||
2
|
||||
</span>
|
||||
<span>Click on your service, then go to "Variables"</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-surface-secondary text-foreground-secondary text-sm flex items-center justify-center">
|
||||
3
|
||||
</span>
|
||||
<span>
|
||||
Add <code className="px-1 bg-surface rounded">EXECUTOR_API_KEY</code> with a
|
||||
secure random value
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-surface-secondary text-foreground-secondary text-sm flex items-center justify-center">
|
||||
4
|
||||
</span>
|
||||
<span>Railway will automatically redeploy with the new variable</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* Environment Variables */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">Environment Variables</h2>
|
||||
<p className="text-foreground-secondary mb-4">
|
||||
Add environment variables via the Railway dashboard or CLI:
|
||||
</p>
|
||||
<CodeBlock language="bash" code={envVars} />
|
||||
<p className="text-sm text-foreground-tertiary mt-4">
|
||||
These variables will be available during tool execution.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Execute a Tool */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">Execute a Tool</h2>
|
||||
<p className="text-foreground-secondary mb-4">
|
||||
Test tool execution with a curl request:
|
||||
</p>
|
||||
<CodeBlock language="bash" code={executeExample} />
|
||||
</section>
|
||||
|
||||
{/* Local Development */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">Local Development</h2>
|
||||
<p className="text-foreground-secondary mb-4">Run the executor locally for testing:</p>
|
||||
<CodeBlock language="bash" code={localDev} />
|
||||
</section>
|
||||
|
||||
{/* Docker */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">Docker Deployment</h2>
|
||||
<p className="text-foreground-secondary mb-4">
|
||||
The template includes a Dockerfile for container deployments:
|
||||
</p>
|
||||
<CodeBlock language="bash" code={dockerDeploy} />
|
||||
<p className="text-sm text-foreground-tertiary mt-4">
|
||||
Railway will automatically detect and use the Dockerfile if present.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* How It Works */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">How It Works</h2>
|
||||
<p className="text-foreground-secondary mb-4">
|
||||
The Railway executor runs as an always-on Node.js service:
|
||||
</p>
|
||||
<ol className="text-foreground-secondary space-y-3">
|
||||
<li className="flex gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary text-sm flex items-center justify-center">
|
||||
1
|
||||
</span>
|
||||
<span>
|
||||
Receives tool execution request via HTTP POST to{' '}
|
||||
<code className="px-1 bg-surface rounded">/execute-tool</code>
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary text-sm flex items-center justify-center">
|
||||
2
|
||||
</span>
|
||||
<span>Creates an isolated temporary directory for the execution</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary text-sm flex items-center justify-center">
|
||||
3
|
||||
</span>
|
||||
<span>
|
||||
Installs the npm package using{' '}
|
||||
<code className="px-1 bg-surface rounded">npm install</code>
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary text-sm flex items-center justify-center">
|
||||
4
|
||||
</span>
|
||||
<span>
|
||||
Loads the tool and calls its{' '}
|
||||
<code className="px-1 bg-surface rounded">execute()</code> function
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex gap-3">
|
||||
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary text-sm flex items-center justify-center">
|
||||
5
|
||||
</span>
|
||||
<span>Returns the result and cleans up the temporary directory</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* Security */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">Security</h2>
|
||||
<ul className="text-foreground-secondary space-y-2">
|
||||
<li className="flex items-start gap-2">
|
||||
<Icon icon="check" className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span>
|
||||
Set <code className="px-1 bg-surface rounded">EXECUTOR_API_KEY</code> to require
|
||||
authentication
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Icon icon="check" className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span>Each tool execution uses an isolated temporary directory</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Icon icon="check" className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span>Environment variables stored encrypted by Railway</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Icon icon="check" className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span>All traffic encrypted via HTTPS</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<Icon icon="check" className="w-5 h-5 text-success flex-shrink-0 mt-0.5" />
|
||||
<span>Auto-restart on failure for high availability</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* Pricing */}
|
||||
<section className="mb-12">
|
||||
<h2 className="text-2xl font-semibold text-foreground mb-4">Pricing</h2>
|
||||
<p className="text-foreground-secondary mb-4">
|
||||
Railway offers usage-based pricing with a generous free tier:
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border">
|
||||
<th className="text-left py-3 pr-4 font-medium text-foreground">Tier</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-foreground">Price</th>
|
||||
<th className="text-left py-3 pl-4 font-medium text-foreground">Includes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="text-foreground-secondary">
|
||||
<tr className="border-b border-border/50">
|
||||
<td className="py-3 pr-4">Free Tier</td>
|
||||
<td className="py-3 px-4">$0/month</td>
|
||||
<td className="py-3 pl-4">$5 credit, enough for light usage</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="py-3 pr-4">Usage-based</td>
|
||||
<td className="py-3 px-4">~$0.000463/min</td>
|
||||
<td className="py-3 pl-4">0.5 vCPU, 512MB RAM</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-tertiary mt-4">
|
||||
See{' '}
|
||||
<a
|
||||
href="https://railway.app/pricing"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Railway Pricing
|
||||
</a>{' '}
|
||||
for current rates.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Connect to TPMJS */}
|
||||
<section className="mb-12 p-6 bg-primary/5 border border-primary/20 rounded-lg">
|
||||
<h2 className="text-lg font-semibold text-foreground mb-4">Connect to TPMJS</h2>
|
||||
<ol className="text-foreground-secondary space-y-2">
|
||||
<li>1. Go to your collection or agent settings on TPMJS</li>
|
||||
<li>2. Select "Custom Executor" in Executor Configuration</li>
|
||||
<li>
|
||||
3. Enter URL:{' '}
|
||||
<code className="px-1.5 py-0.5 bg-surface rounded">
|
||||
https://your-project.up.railway.app
|
||||
</code>
|
||||
</li>
|
||||
<li>4. Enter your API key (if configured)</li>
|
||||
<li>5. Click "Verify Connection"</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="flex items-center justify-between pt-8 border-t border-border">
|
||||
<Link
|
||||
href="/docs/executors"
|
||||
className="flex items-center gap-2 text-foreground-secondary hover:text-foreground transition-colors"
|
||||
>
|
||||
<Icon icon="chevronLeft" className="w-4 h-4" />
|
||||
<span>Back to Executors</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs/executors/unsandbox"
|
||||
className="flex items-center gap-2 text-foreground-secondary hover:text-foreground transition-colors"
|
||||
>
|
||||
<span>Unsandbox Guide</span>
|
||||
<Icon icon="chevronRight" className="w-4 h-4" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<AppFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -397,11 +397,11 @@ export default function UnsandboxExecutorPage(): React.ReactElement {
|
|||
{/* Navigation */}
|
||||
<div className="flex items-center justify-between pt-8 border-t border-border">
|
||||
<Link
|
||||
href="/docs/executors"
|
||||
href="/docs/executors/railway"
|
||||
className="flex items-center gap-2 text-foreground-secondary hover:text-foreground transition-colors"
|
||||
>
|
||||
<Icon icon="chevronLeft" className="w-4 h-4" />
|
||||
<span>Back to Executors</span>
|
||||
<span>Railway Guide</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs/executors/vercel"
|
||||
|
|
|
|||
4
templates/railway-executor/.gitignore
vendored
Normal file
4
templates/railway-executor/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
node_modules/
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
22
templates/railway-executor/Dockerfile
Normal file
22
templates/railway-executor/Dockerfile
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
FROM node:20-slim
|
||||
|
||||
# Install npm for tool installation during execution
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy executor files
|
||||
COPY package.json index.js ./
|
||||
|
||||
# Create temp directory for tool executions
|
||||
RUN mkdir -p /tmp
|
||||
|
||||
# Set environment
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "index.js"]
|
||||
260
templates/railway-executor/README.md
Normal file
260
templates/railway-executor/README.md
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
# TPMJS Executor for Railway
|
||||
|
||||
Deploy your own TPMJS tool executor on **Railway** for reliable, always-on execution.
|
||||
|
||||
## Features
|
||||
|
||||
- **One-Click Deploy**: Deploy to Railway in seconds
|
||||
- **Always-On**: No cold starts, instant tool execution
|
||||
- **Full Control**: Your infrastructure, your environment variables
|
||||
- **Privacy**: No data passes through TPMJS servers
|
||||
- **Auto-Scaling**: Railway handles scaling automatically
|
||||
- **Free Tier**: $5/month free credit included
|
||||
|
||||
## One-Click Deploy
|
||||
|
||||
[](https://railway.app/template/tpmjs-executor?referralCode=tpmjs)
|
||||
|
||||
Or deploy manually:
|
||||
|
||||
```bash
|
||||
# Clone this template
|
||||
git clone https://github.com/tpmjs/tpmjs.git
|
||||
cd tpmjs/templates/railway-executor
|
||||
|
||||
# Create a new Railway project
|
||||
railway init
|
||||
|
||||
# Deploy
|
||||
railway up
|
||||
```
|
||||
|
||||
Your executor will be available at: `https://your-project.up.railway.app`
|
||||
|
||||
## How It Works
|
||||
|
||||
This executor runs as an always-on Node.js service on Railway:
|
||||
|
||||
1. Receives tool execution requests via HTTP
|
||||
2. Installs the npm package in an isolated directory
|
||||
3. Executes the tool with your parameters
|
||||
4. Returns the result and cleans up
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### GET /health
|
||||
|
||||
Check executor health status.
|
||||
|
||||
```bash
|
||||
curl https://your-executor.up.railway.app/health
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.0.0",
|
||||
"info": {
|
||||
"runtime": "railway",
|
||||
"timestamp": "2024-01-01T00:00:00.000Z",
|
||||
"region": "us-west1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /execute-tool
|
||||
|
||||
Execute a TPMJS tool.
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-executor.up.railway.app/execute-tool \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-api-key" \
|
||||
-d '{
|
||||
"packageName": "@tpmjs/hello",
|
||||
"name": "helloWorldTool",
|
||||
"version": "latest",
|
||||
"params": { "includeTimestamp": true }
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"output": {
|
||||
"message": "Hello, World!",
|
||||
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||
},
|
||||
"executionTimeMs": 2345
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `EXECUTOR_API_KEY` | No | API key for authentication. If set, requests must include `Authorization: Bearer <key>` header. |
|
||||
|
||||
### Setting Up API Key Authentication
|
||||
|
||||
1. Go to your Railway project dashboard
|
||||
2. Click on your service
|
||||
3. Go to "Variables" tab
|
||||
4. Add `EXECUTOR_API_KEY` with a secure random value
|
||||
5. The service will automatically redeploy
|
||||
|
||||
### Adding Tool Environment Variables
|
||||
|
||||
Pass environment variables that your tools need:
|
||||
|
||||
1. In Railway dashboard, go to "Variables"
|
||||
2. Add your variables (e.g., `OPENAI_API_KEY`, `DATABASE_URL`)
|
||||
3. These will be available during tool execution
|
||||
|
||||
Or use the Railway CLI:
|
||||
|
||||
```bash
|
||||
railway variables set EXECUTOR_API_KEY=your-key
|
||||
railway variables set OPENAI_API_KEY=sk-xxx
|
||||
railway variables set DATABASE_URL=postgres://...
|
||||
```
|
||||
|
||||
## Connecting to TPMJS
|
||||
|
||||
1. Go to your TPMJS collection or agent settings
|
||||
2. In "Executor Configuration", select "Custom Executor"
|
||||
3. Enter your executor URL: `https://your-project.up.railway.app`
|
||||
4. Enter your API key (if configured)
|
||||
5. Click "Verify Connection" to test
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/tpmjs/tpmjs.git
|
||||
cd tpmjs/templates/railway-executor
|
||||
|
||||
# Run locally
|
||||
PORT=3000 node index.js
|
||||
|
||||
# Or with an API key
|
||||
EXECUTOR_API_KEY=test-key PORT=3000 node index.js
|
||||
|
||||
# Test health endpoint
|
||||
curl http://localhost:3000/health
|
||||
|
||||
# Test tool execution
|
||||
curl -X POST http://localhost:3000/execute-tool \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"packageName": "@tpmjs/hello",
|
||||
"name": "helloWorldTool",
|
||||
"params": {}
|
||||
}'
|
||||
```
|
||||
|
||||
## Managing Your Service
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
railway logs
|
||||
```
|
||||
|
||||
Or view in the Railway dashboard under "Deployments" → select deployment → "Logs"
|
||||
|
||||
### Redeploy
|
||||
|
||||
```bash
|
||||
railway up
|
||||
```
|
||||
|
||||
Or push to your connected GitHub repository for automatic deployments.
|
||||
|
||||
### Scale Resources
|
||||
|
||||
1. Go to Railway dashboard
|
||||
2. Click on your service
|
||||
3. Go to "Settings" tab
|
||||
4. Adjust CPU and memory limits
|
||||
|
||||
### Custom Domains
|
||||
|
||||
1. Go to Railway dashboard
|
||||
2. Click on your service
|
||||
3. Go to "Settings" tab
|
||||
4. Under "Domains", click "Generate Domain" or add a custom domain
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
If you prefer Docker:
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t tpmjs-executor .
|
||||
|
||||
# Run locally
|
||||
docker run -p 3000:3000 -e EXECUTOR_API_KEY=your-key tpmjs-executor
|
||||
```
|
||||
|
||||
Railway will automatically detect and use the Dockerfile if present.
|
||||
|
||||
## Security
|
||||
|
||||
- Set `EXECUTOR_API_KEY` to require authentication for all requests
|
||||
- Tools run in isolated temporary directories
|
||||
- Each execution uses a fresh npm install
|
||||
- Environment variables are stored encrypted by Railway
|
||||
- Network traffic is encrypted via HTTPS
|
||||
|
||||
## Pricing
|
||||
|
||||
Railway pricing is usage-based with a generous free tier:
|
||||
|
||||
- **Free Tier**: $5/month credit (enough for light usage)
|
||||
- **Pay-as-you-go**: ~$0.000463/min for 0.5 vCPU, 512MB RAM
|
||||
|
||||
See [Railway Pricing](https://railway.app/pricing) for current rates.
|
||||
|
||||
**Cost Optimization Tips:**
|
||||
- Use the "Sleep" feature for dev environments
|
||||
- Set memory limits appropriate for your tools
|
||||
- Monitor usage in Railway dashboard
|
||||
|
||||
## Comparison: Railway vs Other Platforms
|
||||
|
||||
| Feature | Railway | Vercel | Unsandbox |
|
||||
|---------|---------|--------|-----------|
|
||||
| Deploy method | One-click / CLI | One-click | CLI |
|
||||
| Cold starts | None (always-on) | Yes (serverless) | None |
|
||||
| Max runtime | Unlimited | 45min / 5hr | Unlimited |
|
||||
| Free tier | $5/month credit | Limited | None |
|
||||
| Pricing | Per usage | Per compute | Per uptime |
|
||||
| Docker support | Yes | No | Yes |
|
||||
| Auto-scaling | Yes | Yes | Manual |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Connection refused" errors
|
||||
- Check that your service is running in Railway dashboard
|
||||
- Verify the URL is correct (check "Domains" in settings)
|
||||
- Ensure `EXECUTOR_API_KEY` matches if authentication is enabled
|
||||
|
||||
### Tool installation failures
|
||||
- Check Railway logs for npm errors
|
||||
- Verify the package name and version are correct
|
||||
- Some packages may need additional system dependencies
|
||||
|
||||
### Timeout errors
|
||||
- Railway has no timeout limit, but individual tool executions timeout at 2 minutes
|
||||
- For longer-running tools, consider increasing the timeout in the executor code
|
||||
|
||||
## Support
|
||||
|
||||
- [TPMJS Custom Executors Documentation](https://tpmjs.com/docs/executors)
|
||||
- [Railway Documentation](https://docs.railway.app)
|
||||
- [GitHub Issues](https://github.com/tpmjs/tpmjs/issues)
|
||||
398
templates/railway-executor/index.js
Normal file
398
templates/railway-executor/index.js
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* TPMJS Executor for Railway
|
||||
*
|
||||
* A lightweight HTTP server that executes TPMJS tools.
|
||||
* Designed for deployment on Railway with zero dependencies.
|
||||
*
|
||||
* API-compatible with the Vercel and Unsandbox executors.
|
||||
*/
|
||||
|
||||
const http = require('node:http');
|
||||
const { execSync, spawn } = require('node:child_process');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const API_KEY = process.env.EXECUTOR_API_KEY || null;
|
||||
|
||||
// CORS headers for cross-origin requests
|
||||
const corsHeaders = {
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||
};
|
||||
|
||||
/**
|
||||
* Send JSON response with proper headers
|
||||
*/
|
||||
function jsonResponse(res, statusCode, data) {
|
||||
res.writeHead(statusCode, {
|
||||
'Content-Type': 'application/json',
|
||||
...corsHeaders,
|
||||
});
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate API key if configured
|
||||
*/
|
||||
function checkAuth(req) {
|
||||
if (!API_KEY) return true;
|
||||
const authHeader = req.headers.authorization;
|
||||
return authHeader === `Bearer ${API_KEY}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JSON request body
|
||||
*/
|
||||
function parseBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
req.on('data', (chunk) => {
|
||||
body += chunk;
|
||||
});
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolve(body ? JSON.parse(body) : {});
|
||||
} catch (_e) {
|
||||
reject(new Error('Invalid JSON'));
|
||||
}
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /health - Health check endpoint
|
||||
*/
|
||||
function handleHealth(_req, res) {
|
||||
jsonResponse(res, 200, {
|
||||
status: 'ok',
|
||||
version: '1.0.0',
|
||||
info: {
|
||||
runtime: 'railway',
|
||||
timestamp: new Date().toISOString(),
|
||||
region: process.env.RAILWAY_REGION || 'unknown',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create isolated work directory and package.json
|
||||
*/
|
||||
function createWorkDir() {
|
||||
const workDir = `/tmp/tpmjs-exec-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
fs.mkdirSync(workDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(workDir, 'package.json'),
|
||||
JSON.stringify({
|
||||
name: 'tpmjs-execution',
|
||||
private: true,
|
||||
type: 'commonjs',
|
||||
})
|
||||
);
|
||||
return workDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install npm package in work directory
|
||||
*/
|
||||
function installPackage(workDir, packageSpec) {
|
||||
execSync(`npm install --no-save --omit=dev --no-audit --no-fund ${packageSpec}`, {
|
||||
cwd: workDir,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
timeout: 60000, // 60s timeout for install
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the tool execution script
|
||||
*/
|
||||
function generateExecutionScript(packageName, name, params, env) {
|
||||
const envSetup = env
|
||||
? Object.entries(env)
|
||||
.map(([key, value]) => `process.env[${JSON.stringify(key)}] = ${JSON.stringify(value)};`)
|
||||
.join('\n')
|
||||
: '';
|
||||
|
||||
return `
|
||||
${envSetup}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const pkg = require(${JSON.stringify(packageName)});
|
||||
|
||||
// Find the tool export - check named export, default.name, or default
|
||||
let tool = pkg[${JSON.stringify(name)}] || pkg.default?.[${JSON.stringify(name)}] || pkg.default;
|
||||
|
||||
if (!tool) {
|
||||
throw new Error(\`Tool "${name}" not found in package "${packageName}"\`);
|
||||
}
|
||||
|
||||
// Handle factory functions (tools that need to be instantiated)
|
||||
if (typeof tool === 'function' && !tool.execute) {
|
||||
const envVars = ${env ? JSON.stringify(env) : 'null'};
|
||||
|
||||
// Try no-arg call first
|
||||
try {
|
||||
const result = tool();
|
||||
if (result && typeof result.execute === 'function') {
|
||||
tool = result;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Try with env config if still a function
|
||||
if (typeof tool === 'function' && envVars) {
|
||||
try {
|
||||
const result = tool(envVars);
|
||||
if (result && typeof result.execute === 'function') {
|
||||
tool = result;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
if (!tool || typeof tool.execute !== 'function') {
|
||||
throw new Error(\`Tool "${name}" does not have an execute() function\`);
|
||||
}
|
||||
|
||||
// Execute the tool
|
||||
const result = await tool.execute(${JSON.stringify(params)});
|
||||
process.stdout.write(JSON.stringify({ __tpmjs_result__: result }));
|
||||
} catch (err) {
|
||||
process.stderr.write(JSON.stringify({ __tpmjs_error__: err.message || String(err) }));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
})();
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run execution script and return results
|
||||
*/
|
||||
function runScript(workDir, env) {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn('node', ['execute.cjs'], {
|
||||
cwd: workDir,
|
||||
env: { ...process.env, ...env },
|
||||
timeout: 120000, // 2 minute timeout
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
stdout += data;
|
||||
});
|
||||
child.stderr.on('data', (data) => {
|
||||
stderr += data;
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve({ exitCode: code, stdout, stderr });
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
resolve({ exitCode: 1, stdout: '', stderr: err.message });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up work directory
|
||||
*/
|
||||
function cleanup(workDir) {
|
||||
try {
|
||||
fs.rmSync(workDir, { recursive: true, force: true });
|
||||
} catch (_e) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse execution result and determine response
|
||||
*/
|
||||
function parseExecutionResult(result, startTime) {
|
||||
// Handle execution failure
|
||||
if (result.exitCode !== 0) {
|
||||
// Try to parse structured error from stderr
|
||||
try {
|
||||
const errorObj = JSON.parse(result.stderr);
|
||||
if (errorObj.__tpmjs_error__) {
|
||||
return {
|
||||
success: false,
|
||||
error: errorObj.__tpmjs_error__,
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
} catch (_e) {
|
||||
// Not structured error
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: result.stderr || `Script exited with code ${result.exitCode}`,
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
// Parse the result
|
||||
try {
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
if (parsed.__tpmjs_result__ !== undefined) {
|
||||
return {
|
||||
success: true,
|
||||
output: parsed.__tpmjs_result__,
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
} catch (_e) {
|
||||
// Not structured result
|
||||
}
|
||||
|
||||
// Return raw output
|
||||
return {
|
||||
success: true,
|
||||
output: result.stdout || null,
|
||||
stderr: result.stderr || undefined,
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /execute-tool - Execute a TPMJS tool
|
||||
*/
|
||||
async function handleExecuteTool(req, res) {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Check authorization
|
||||
if (!checkAuth(req)) {
|
||||
return jsonResponse(res, 401, {
|
||||
success: false,
|
||||
error: 'Unauthorized',
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
});
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
let body;
|
||||
try {
|
||||
body = await parseBody(req);
|
||||
} catch (_e) {
|
||||
return jsonResponse(res, 400, {
|
||||
success: false,
|
||||
error: 'Invalid JSON body',
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
});
|
||||
}
|
||||
|
||||
const { packageName, name, version = 'latest', params = {}, env } = body;
|
||||
|
||||
// Validate required fields
|
||||
if (!packageName || !name) {
|
||||
return jsonResponse(res, 400, {
|
||||
success: false,
|
||||
error: 'Missing required fields: packageName, name',
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
});
|
||||
}
|
||||
|
||||
const packageSpec = `${packageName}@${version}`;
|
||||
const workDir = createWorkDir();
|
||||
|
||||
try {
|
||||
// Install the npm package
|
||||
console.log(`[executor] Installing ${packageSpec}...`);
|
||||
const installStart = Date.now();
|
||||
|
||||
try {
|
||||
installPackage(workDir, packageSpec);
|
||||
} catch (installError) {
|
||||
console.error(`[executor] npm install failed:`, installError.message);
|
||||
cleanup(workDir);
|
||||
return jsonResponse(res, 500, {
|
||||
success: false,
|
||||
error: `npm install failed: ${installError.message}`,
|
||||
stderr: installError.stderr?.toString(),
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[executor] npm install completed in ${Date.now() - installStart}ms`);
|
||||
|
||||
// Generate and write execution script
|
||||
const script = generateExecutionScript(packageName, name, params, env);
|
||||
fs.writeFileSync(path.join(workDir, 'execute.cjs'), script);
|
||||
|
||||
// Run the execution script
|
||||
console.log(`[executor] Running tool ${packageName}/${name}...`);
|
||||
const runStart = Date.now();
|
||||
const result = await runScript(workDir, env);
|
||||
console.log(
|
||||
`[executor] Tool execution completed in ${Date.now() - runStart}ms (exit: ${result.exitCode})`
|
||||
);
|
||||
|
||||
// Cleanup and return result
|
||||
cleanup(workDir);
|
||||
return jsonResponse(res, 200, parseExecutionResult(result, startTime));
|
||||
} catch (error) {
|
||||
cleanup(workDir);
|
||||
return jsonResponse(res, 500, {
|
||||
success: false,
|
||||
error: error.message || String(error),
|
||||
executionTimeMs: Date.now() - startTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main HTTP server
|
||||
*/
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url, `http://localhost:${PORT}`);
|
||||
const pathname = url.pathname;
|
||||
|
||||
// Handle CORS preflight
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200, corsHeaders);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
// Route requests (support both /api/path and /path)
|
||||
if ((pathname === '/api/health' || pathname === '/health') && req.method === 'GET') {
|
||||
return handleHealth(req, res);
|
||||
}
|
||||
|
||||
if ((pathname === '/api/execute-tool' || pathname === '/execute-tool') && req.method === 'POST') {
|
||||
return handleExecuteTool(req, res);
|
||||
}
|
||||
|
||||
// Root path - simple info
|
||||
if (pathname === '/' && req.method === 'GET') {
|
||||
return jsonResponse(res, 200, {
|
||||
name: 'TPMJS Executor',
|
||||
version: '1.0.0',
|
||||
runtime: 'railway',
|
||||
endpoints: {
|
||||
health: 'GET /health',
|
||||
execute: 'POST /execute-tool',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 404 for unknown routes
|
||||
jsonResponse(res, 404, { error: 'Not found' });
|
||||
});
|
||||
|
||||
// Start server
|
||||
server.listen(PORT, () => {
|
||||
console.log(`TPMJS Executor running on port ${PORT}`);
|
||||
console.log(`Health: http://localhost:${PORT}/health`);
|
||||
console.log(`Execute: POST http://localhost:${PORT}/execute-tool`);
|
||||
if (API_KEY) {
|
||||
console.log(`Authentication: Required (EXECUTOR_API_KEY is set)`);
|
||||
} else {
|
||||
console.log(`Authentication: None (set EXECUTOR_API_KEY to enable)`);
|
||||
}
|
||||
});
|
||||
20
templates/railway-executor/package.json
Normal file
20
templates/railway-executor/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "tpmjs-executor",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "TPMJS Tool Executor for Railway - Deploy your own executor on Railway",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"dev": "node index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"dependencies": {},
|
||||
"keywords": [
|
||||
"tpmjs",
|
||||
"executor",
|
||||
"railway"
|
||||
]
|
||||
}
|
||||
13
templates/railway-executor/railway.json
Normal file
13
templates/railway-executor/railway.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"$schema": "https://railway.app/railway.schema.json",
|
||||
"build": {
|
||||
"builder": "NIXPACKS"
|
||||
},
|
||||
"deploy": {
|
||||
"startCommand": "node index.js",
|
||||
"healthcheckPath": "/health",
|
||||
"healthcheckTimeout": 30,
|
||||
"restartPolicyType": "ON_FAILURE",
|
||||
"restartPolicyMaxRetries": 3
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue