@agent-foundry/replay-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+ # Build a game and copy to bundles directory
3
+ # Usage: ./build-bundle.sh game-life-restart
4
+ #
5
+ # This script:
6
+ # 1. Builds the specified game from repo/
7
+ # 2. Copies the dist/ output to bundles/<game-name>/
8
+
9
+ set -e
10
+
11
+ GAME_NAME=$1
12
+
13
+ if [ -z "$GAME_NAME" ]; then
14
+ echo "Usage: $0 <game-name>"
15
+ echo "Example: $0 game-life-restart"
16
+ exit 1
17
+ fi
18
+
19
+ # Get the repo root (3 levels up from this script)
20
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
21
+ REPO_ROOT=$(cd "$SCRIPT_DIR/../../.." && pwd)
22
+ GAME_DIR="$REPO_ROOT/repo/$GAME_NAME"
23
+ BUNDLES_DIR="$SCRIPT_DIR/../bundles"
24
+
25
+ # Verify game exists
26
+ if [ ! -d "$GAME_DIR" ]; then
27
+ echo "Error: Game directory not found: $GAME_DIR"
28
+ exit 1
29
+ fi
30
+
31
+ echo "📦 Building $GAME_NAME..."
32
+ echo " Game directory: $GAME_DIR"
33
+ echo " Bundles directory: $BUNDLES_DIR"
34
+
35
+ # Build the game with base path for bundle serving
36
+ BASE_PATH="/bundles/$GAME_NAME/"
37
+ echo "📦 Building with base path: $BASE_PATH"
38
+ cd "$GAME_DIR"
39
+ pnpm install
40
+ VITE_BASE_PATH="$BASE_PATH" pnpm build
41
+
42
+ # Create bundle directory
43
+ mkdir -p "$BUNDLES_DIR/$GAME_NAME"
44
+
45
+ # Copy build output
46
+ echo "📋 Copying build output to bundles..."
47
+ cp -r dist/* "$BUNDLES_DIR/$GAME_NAME/"
48
+
49
+ echo "✅ Bundle created: $BUNDLES_DIR/$GAME_NAME"
50
+ echo ""
51
+ echo "To use this bundle, set bundleId in your manifest:"
52
+ echo ' { "bundleId": "'$GAME_NAME'" }'
@@ -0,0 +1,81 @@
1
+ #!/bin/bash
2
+ # Deploy replay-server to Aliyun Container Registry (ACR)
3
+ #
4
+ # Prerequisites:
5
+ # 1. Docker installed and running
6
+ # 2. Logged into ACR: docker login registry.cn-beijing.aliyuncs.com
7
+ #
8
+ # Usage:
9
+ # ./deploy-aliyun.sh
10
+ # ./deploy-aliyun.sh <custom-tag>
11
+ #
12
+ # Environment variables (optional):
13
+ # ACR_REGISTRY - ACR registry URL (default: registry.cn-beijing.aliyuncs.com)
14
+ # ACR_NAMESPACE - Your ACR namespace (required)
15
+ # IMAGE_NAME - Image name (default: replay-server)
16
+
17
+ set -e
18
+
19
+ # Configuration
20
+ ACR_REGISTRY="${ACR_REGISTRY:-registry.cn-beijing.aliyuncs.com}"
21
+ ACR_NAMESPACE="${ACR_NAMESPACE:-}"
22
+ IMAGE_NAME="${IMAGE_NAME:-replay-server}"
23
+ VERSION="${1:-$(date +%Y%m%d-%H%M%S)}"
24
+
25
+ # Validate namespace
26
+ if [ -z "$ACR_NAMESPACE" ]; then
27
+ echo "Error: ACR_NAMESPACE environment variable is required"
28
+ echo ""
29
+ echo "Usage:"
30
+ echo " ACR_NAMESPACE=your-namespace ./deploy-aliyun.sh"
31
+ echo ""
32
+ echo "Or set it in your environment:"
33
+ echo " export ACR_NAMESPACE=your-namespace"
34
+ exit 1
35
+ fi
36
+
37
+ # Get repo root
38
+ SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
39
+ REPO_ROOT=$(cd "$SCRIPT_DIR/../../.." && pwd)
40
+
41
+ echo "🚀 Deploying replay-server to Aliyun ACR"
42
+ echo " Registry: $ACR_REGISTRY"
43
+ echo " Namespace: $ACR_NAMESPACE"
44
+ echo " Image: $IMAGE_NAME"
45
+ echo " Version: $VERSION"
46
+ echo ""
47
+
48
+ # Build from project root
49
+ cd "$REPO_ROOT"
50
+
51
+ FULL_IMAGE="${ACR_REGISTRY}/${ACR_NAMESPACE}/${IMAGE_NAME}"
52
+
53
+ echo "📦 Building Docker image..."
54
+ docker build \
55
+ -f packages/replay-server/Dockerfile \
56
+ -t "${FULL_IMAGE}:${VERSION}" \
57
+ -t "${FULL_IMAGE}:latest" \
58
+ .
59
+
60
+ echo ""
61
+ echo "📤 Pushing to ACR..."
62
+ docker push "${FULL_IMAGE}:${VERSION}"
63
+ docker push "${FULL_IMAGE}:latest"
64
+
65
+ echo ""
66
+ echo "✅ Deployment complete!"
67
+ echo ""
68
+ echo "Image URLs:"
69
+ echo " ${FULL_IMAGE}:${VERSION}"
70
+ echo " ${FULL_IMAGE}:latest"
71
+ echo ""
72
+ echo "Next steps:"
73
+ echo " 1. Go to Aliyun Function Compute console"
74
+ echo " 2. Create/update function with image: ${FULL_IMAGE}:${VERSION}"
75
+ echo " 3. Set environment variables:"
76
+ echo " - PORT=9000"
77
+ echo " - OUTPUT_DIR=/tmp/output"
78
+ echo " 4. Configure function settings:"
79
+ echo " - Memory: 4096 MB"
80
+ echo " - Timeout: 600 seconds"
81
+ echo " - Instance Concurrency: 1"
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI tool for offline video rendering
4
+ *
5
+ * Usage:
6
+ * npx tsx src/cli/render.ts --manifest ./replay.json --output ./output.mp4
7
+ * npx tsx src/cli/render.ts --manifest-url https://... --game-url http://localhost:5173
8
+ */
9
+
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { PuppeteerRenderer, RenderProgress } from '../renderer/PuppeteerRenderer.js';
13
+ import type { ReplayManifestV1 } from '@agent-foundry/replay';
14
+
15
+ interface CliArgs {
16
+ manifest?: string;
17
+ manifestUrl?: string;
18
+ output: string;
19
+ gameUrl: string;
20
+ width: number;
21
+ height: number;
22
+ fps: number;
23
+ secondsPerAge: number;
24
+ keepTemp: boolean;
25
+ useHardwareAcceleration: boolean;
26
+ help: boolean;
27
+ }
28
+
29
+ function parseArgs(): CliArgs {
30
+ const args = process.argv.slice(2);
31
+ const result: CliArgs = {
32
+ output: './output.mp4',
33
+ gameUrl: 'http://localhost:5173',
34
+ width: 720,
35
+ height: 1280,
36
+ fps: 16,
37
+ secondsPerAge: 1.0,
38
+ keepTemp: false,
39
+ useHardwareAcceleration: false,
40
+ help: false,
41
+ };
42
+
43
+ for (let i = 0; i < args.length; i++) {
44
+ const arg = args[i];
45
+ const next = args[i + 1];
46
+
47
+ switch (arg) {
48
+ case '--manifest':
49
+ case '-m':
50
+ result.manifest = next;
51
+ i++;
52
+ break;
53
+ case '--manifest-url':
54
+ result.manifestUrl = next;
55
+ i++;
56
+ break;
57
+ case '--output':
58
+ case '-o':
59
+ result.output = next;
60
+ i++;
61
+ break;
62
+ case '--game-url':
63
+ case '-g':
64
+ result.gameUrl = next;
65
+ i++;
66
+ break;
67
+ case '--width':
68
+ result.width = parseInt(next, 10);
69
+ i++;
70
+ break;
71
+ case '--height':
72
+ result.height = parseInt(next, 10);
73
+ i++;
74
+ break;
75
+ case '--fps':
76
+ result.fps = parseInt(next, 10);
77
+ i++;
78
+ break;
79
+ case '--seconds-per-age':
80
+ result.secondsPerAge = parseFloat(next);
81
+ i++;
82
+ break;
83
+ case '--keep-temp':
84
+ result.keepTemp = true;
85
+ break;
86
+ case '--use-hardware-acceleration':
87
+ case '--nvenc':
88
+ result.useHardwareAcceleration = true;
89
+ break;
90
+ case '--help':
91
+ case '-h':
92
+ result.help = true;
93
+ break;
94
+ }
95
+ }
96
+
97
+ return result;
98
+ }
99
+
100
+ function printHelp(): void {
101
+ console.log(`
102
+ 🎬 LifeRestart Replay Renderer CLI
103
+
104
+ Usage:
105
+ npx tsx src/cli/render.ts [options]
106
+
107
+ Options:
108
+ -m, --manifest <path> Path to replay manifest JSON file
109
+ --manifest-url <url> URL to fetch replay manifest from
110
+ -o, --output <path> Output video path (default: ./output.mp4)
111
+ -g, --game-url <url> Game URL (default: http://localhost:5173)
112
+ --width <px> Video width (default: 720)
113
+ --height <px> Video height (default: 1280)
114
+ --fps <n> Frames per second (default: 16)
115
+ --seconds-per-age <n> Seconds to display each age (default: 1.0)
116
+ --use-hardware-acceleration Enable NVENC hardware acceleration (default: CPU encoding)
117
+ --nvenc Alias for --use-hardware-acceleration
118
+ --keep-temp Keep temporary screenshot files (deprecated)
119
+ -h, --help Show this help message
120
+
121
+ Examples:
122
+ # Render from local manifest file
123
+ npx tsx src/cli/render.ts -m ./replay.json -o ./video.mp4
124
+
125
+ # Render from URL
126
+ npx tsx src/cli/render.ts --manifest-url https://example.com/replay.json
127
+
128
+ # Custom video settings
129
+ npx tsx src/cli/render.ts -m ./replay.json --width 720 --height 1280 --fps 60
130
+ `);
131
+ }
132
+
133
+ async function loadManifest(args: CliArgs): Promise<ReplayManifestV1> {
134
+ if (args.manifest) {
135
+ const manifestPath = path.resolve(args.manifest);
136
+ if (!fs.existsSync(manifestPath)) {
137
+ throw new Error(`Manifest file not found: ${manifestPath}`);
138
+ }
139
+ const content = await fs.promises.readFile(manifestPath, 'utf-8');
140
+ return JSON.parse(content);
141
+ }
142
+
143
+ if (args.manifestUrl) {
144
+ const response = await fetch(args.manifestUrl);
145
+ if (!response.ok) {
146
+ throw new Error(`Failed to fetch manifest: ${response.statusText}`);
147
+ }
148
+ return await response.json();
149
+ }
150
+
151
+ throw new Error('Either --manifest or --manifest-url is required');
152
+ }
153
+
154
+ function formatProgress(progress: RenderProgress): string {
155
+ const percent = progress.total > 0
156
+ ? Math.round((progress.current / progress.total) * 100)
157
+ : 0;
158
+
159
+ const bar = '█'.repeat(Math.floor(percent / 5)) + '░'.repeat(20 - Math.floor(percent / 5));
160
+
161
+ return `[${bar}] ${percent}% - ${progress.message}`;
162
+ }
163
+
164
+ async function main(): Promise<void> {
165
+ const args = parseArgs();
166
+
167
+ if (args.help) {
168
+ printHelp();
169
+ process.exit(0);
170
+ }
171
+
172
+ if (!args.manifest && !args.manifestUrl) {
173
+ console.error('Error: Either --manifest or --manifest-url is required');
174
+ console.error('Run with --help for usage information');
175
+ process.exit(1);
176
+ }
177
+
178
+ console.log('🎬 LifeRestart Replay Renderer');
179
+ console.log('━'.repeat(50));
180
+
181
+ try {
182
+ // Load manifest
183
+ console.log('📄 Loading manifest...');
184
+ const manifest = await loadManifest(args);
185
+ console.log(` Game ID: ${manifest.gameId}`);
186
+ console.log(` Timeline: ${manifest.timeline.length} ages`);
187
+ console.log(` Highlights: ${manifest.highlights.length}`);
188
+
189
+ // Estimate duration
190
+ const estimatedDuration = manifest.timeline.length * args.secondsPerAge;
191
+ console.log(` Estimated video duration: ${estimatedDuration}s`);
192
+ console.log('');
193
+
194
+ // Create output directory
195
+ const outputDir = path.dirname(path.resolve(args.output));
196
+ await fs.promises.mkdir(outputDir, { recursive: true });
197
+
198
+ // Start rendering
199
+ console.log('🚀 Starting render...');
200
+ console.log(` Game URL: ${args.gameUrl}`);
201
+ console.log(` Output: ${args.output}`);
202
+ console.log(` Resolution: ${args.width}x${args.height} @ ${args.fps}fps`);
203
+ console.log(` Encoder: ${args.useHardwareAcceleration ? 'h264_nvenc (Hardware)' : 'libx264 (CPU)'}`);
204
+ console.log('');
205
+
206
+ const renderer = new PuppeteerRenderer();
207
+
208
+ let lastLine = '';
209
+ const result = await renderer.render(manifest, {
210
+ gameUrl: args.gameUrl,
211
+ outputPath: path.resolve(args.output),
212
+ width: args.width,
213
+ height: args.height,
214
+ fps: args.fps,
215
+ secondsPerAge: args.secondsPerAge,
216
+ useHardwareAcceleration: args.useHardwareAcceleration,
217
+ keepTempFiles: args.keepTemp,
218
+ onProgress: (progress) => {
219
+ const line = formatProgress(progress);
220
+ if (line !== lastLine) {
221
+ process.stdout.write(`\r${line}`);
222
+ lastLine = line;
223
+ }
224
+ },
225
+ });
226
+
227
+ console.log('\n');
228
+
229
+ if (result.success) {
230
+ console.log('✅ Render completed successfully!');
231
+ console.log(` Output: ${result.outputPath}`);
232
+ console.log(` Duration: ${result.duration?.toFixed(1)}s`);
233
+ } else {
234
+ console.error('❌ Render failed:', result.error);
235
+ process.exit(1);
236
+ }
237
+ } catch (error) {
238
+ console.error('❌ Error:', error instanceof Error ? error.message : error);
239
+ process.exit(1);
240
+ }
241
+ }
242
+
243
+ main();
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Main exports
2
+ export * from './renderer';