Skip to content

Commit 5ee3bde

Browse files
authored
Merge branch 'develop' into feature/multi-step-enhancements
2 parents a1bd4ee + 501dd11 commit 5ee3bde

File tree

10 files changed

+1146
-201
lines changed

10 files changed

+1146
-201
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,7 @@ tsconfig.tsbuildinfo
185185
test-data/
186186
standalone.ts
187187
/.claude
188+
189+
# Continuous Claude cache (local only)
190+
.claude/cache/
191+
/thoughts

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ elizaos test -t component # Run only component tests
255255
elizaos test -t e2e # Run only e2e tests
256256
elizaos test --name "test" # Filter tests by name (case sensitive)
257257
elizaos test --skip-build # Skip building before tests
258+
elizaos test --port <port> # Port for e2e tests
259+
elizaos test --skip-type-check # Skip TypeScript validation
258260
```
259261

260262
**Test Types:**

packages/cli/src/commands/deploy/utils/docker-build.ts

Lines changed: 123 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as fs from 'node:fs';
77
import * as path from 'node:path';
88
import { logger } from '@elizaos/core';
9-
import { execa } from 'execa';
9+
import { bunExec } from '../../../utils/bun-exec';
1010
import crypto from 'node:crypto';
1111
import ora from 'ora';
1212

@@ -49,9 +49,10 @@ export interface DockerPushResult {
4949
*/
5050
export async function checkDockerAvailable(): Promise<boolean> {
5151
try {
52-
await execa('docker', ['--version']);
53-
await execa('docker', ['info']);
54-
return true;
52+
const versionResult = await bunExec('docker', ['--version']);
53+
if (!versionResult.success) return false;
54+
const infoResult = await bunExec('docker', ['info']);
55+
return infoResult.success;
5556
} catch {
5657
return false;
5758
}
@@ -196,13 +197,22 @@ export async function buildDockerImage(options: DockerBuildOptions): Promise<Doc
196197

197198
// Execute Docker build
198199
const startTime = Date.now();
199-
const { stdout } = await execa('docker', buildArgs, {
200+
const buildResult = await bunExec('docker', buildArgs, {
200201
env: {
201-
...process.env,
202202
DOCKER_DEFAULT_PLATFORM: platform,
203203
DOCKER_BUILDKIT: '1',
204204
},
205205
});
206+
207+
if (!buildResult.success) {
208+
return {
209+
success: false,
210+
imageTag: options.imageTag,
211+
error: buildResult.stderr || 'Docker build failed',
212+
};
213+
}
214+
215+
const stdout = buildResult.stdout;
206216
const buildTime = Date.now() - startTime;
207217

208218
logger.debug(
@@ -216,12 +226,20 @@ export async function buildDockerImage(options: DockerBuildOptions): Promise<Doc
216226
}
217227

218228
// Get image info
219-
const inspectResult = await execa('docker', [
229+
const inspectResult = await bunExec('docker', [
220230
'inspect',
221231
options.imageTag,
222232
'--format={{.Id}}|{{.Size}}',
223233
]);
224234

235+
if (!inspectResult.success) {
236+
return {
237+
success: false,
238+
imageTag: options.imageTag,
239+
error: inspectResult.stderr || 'Failed to inspect Docker image',
240+
};
241+
}
242+
225243
const [imageId, sizeStr] = inspectResult.stdout.split('|');
226244
const size = parseInt(sizeStr, 10);
227245

@@ -272,11 +290,26 @@ async function loginToECR(registryUrl: string, authToken: string): Promise<void>
272290
'Logging in to ECR registry'
273291
);
274292

275-
// Docker login
276-
await execa('docker', ['login', '--username', username, '--password-stdin', cleanRegistryUrl], {
277-
input: password,
293+
// Docker login - use Bun.spawn directly for stdin input
294+
const proc = Bun.spawn(['docker', 'login', '--username', username, '--password-stdin', cleanRegistryUrl], {
295+
stdin: 'pipe',
296+
stdout: 'pipe',
297+
stderr: 'pipe',
278298
});
279299

300+
// Write password to stdin using FileSink API
301+
if (proc.stdin) {
302+
proc.stdin.write(password);
303+
proc.stdin.end();
304+
}
305+
306+
const exitCode = await proc.exited;
307+
308+
if (exitCode !== 0) {
309+
const stderr = await new Response(proc.stderr).text();
310+
throw new Error(`Docker login failed: ${stderr}`);
311+
}
312+
280313
logger.info({ src: 'cli', util: 'docker-build' }, 'Logged in to ECR');
281314
}
282315

@@ -286,7 +319,11 @@ async function loginToECR(registryUrl: string, authToken: string): Promise<void>
286319
async function tagImageForECR(localTag: string, ecrImageUri: string): Promise<void> {
287320
logger.info({ src: 'cli', util: 'docker-build', ecrImageUri }, 'Tagging image for ECR');
288321

289-
await execa('docker', ['tag', localTag, ecrImageUri]);
322+
const result = await bunExec('docker', ['tag', localTag, ecrImageUri]);
323+
324+
if (!result.success) {
325+
throw new Error(`Failed to tag image: ${result.stderr}`);
326+
}
290327

291328
logger.debug({ src: 'cli', util: 'docker-build', localTag, ecrImageUri }, 'Image tagged for ECR');
292329
}
@@ -338,66 +375,90 @@ export async function pushDockerImage(options: DockerPushOptions): Promise<Docke
338375
let completedLayers = 0;
339376
const layerProgress = new Map<string, { current: number; total: number }>();
340377

341-
const pushProcess = execa('docker', ['push', ecrImageUri]);
378+
// Use Bun.spawn for the push process with streaming stderr
379+
const pushProcess = Bun.spawn(['docker', 'push', ecrImageUri], {
380+
stdout: 'pipe',
381+
stderr: 'pipe',
382+
});
342383

343-
// Track progress from stderr (Docker outputs progress to stderr)
344-
if (pushProcess.stderr) {
345-
pushProcess.stderr.on('data', (data: Buffer) => {
346-
const output = data.toString();
384+
// Process stderr stream for progress tracking
385+
const processStderr = async () => {
386+
if (!pushProcess.stderr) return;
347387

348-
// Parse Docker layer progress
349-
// Format: "layer-id: Pushing [==> ] 15.5MB/100MB"
350-
const lines = output.split('\n');
388+
const reader = pushProcess.stderr.getReader();
389+
const decoder = new TextDecoder();
351390

352-
for (const line of lines) {
353-
const layerMatch = line.match(
354-
/^([a-f0-9]+):\s*(\w+)\s*\[([=>]+)\s*\]\s+([\d.]+)([KMGT]?B)\/([\d.]+)([KMGT]?B)/
355-
);
391+
try {
392+
while (true) {
393+
const { done, value } = await reader.read();
394+
if (done) break;
356395

357-
if (layerMatch) {
358-
const [, layerId, , , currentStr, currentUnit, totalStr, totalUnit] = layerMatch;
396+
const output = decoder.decode(value, { stream: true });
359397

360-
// Convert to bytes for accurate progress
361-
const current = parseSize(currentStr, currentUnit);
362-
const total = parseSize(totalStr, totalUnit);
398+
// Parse Docker layer progress
399+
// Format: "layer-id: Pushing [==> ] 15.5MB/100MB"
400+
const lines = output.split('\n');
363401

364-
layerProgress.set(layerId, { current, total });
402+
for (const line of lines) {
403+
const layerMatch = line.match(
404+
/^([a-f0-9]+):\s*(\w+)\s*\[([=>]+)\s*\]\s+([\d.]+)([KMGT]?B)\/([\d.]+)([KMGT]?B)/
405+
);
365406

366-
// Calculate overall progress
367-
let totalBytes = 0;
368-
let uploadedBytes = 0;
407+
if (layerMatch) {
408+
const [, layerId, , , currentStr, currentUnit, totalStr, totalUnit] = layerMatch;
369409

370-
for (const [, progress] of layerProgress) {
371-
totalBytes += progress.total;
372-
uploadedBytes += progress.current;
373-
}
410+
// Convert to bytes for accurate progress
411+
const current = parseSize(currentStr, currentUnit);
412+
const total = parseSize(totalStr, totalUnit);
374413

375-
const overallPercent =
376-
totalBytes > 0 ? Math.floor((uploadedBytes / totalBytes) * 100) : 0;
377-
const uploadedMB = (uploadedBytes / 1024 / 1024).toFixed(1);
378-
const totalMB = (totalBytes / 1024 / 1024).toFixed(1);
414+
layerProgress.set(layerId, { current, total });
379415

380-
spinner.text = `Pushing to ECR... ${overallPercent}% (${uploadedMB}/${totalMB} MB, ${layerProgress.size} layers)`;
381-
}
416+
// Calculate overall progress
417+
let totalBytes = 0;
418+
let uploadedBytes = 0;
382419

383-
// Check for pushed layers
384-
if (line.includes(': Pushed')) {
385-
completedLayers++;
386-
}
420+
for (const [, progress] of layerProgress) {
421+
totalBytes += progress.total;
422+
uploadedBytes += progress.current;
423+
}
424+
425+
const overallPercent =
426+
totalBytes > 0 ? Math.floor((uploadedBytes / totalBytes) * 100) : 0;
427+
const uploadedMB = (uploadedBytes / 1024 / 1024).toFixed(1);
428+
const totalMB = (totalBytes / 1024 / 1024).toFixed(1);
387429

388-
// Check for completion digest
389-
const digestMatch = line.match(/digest: (sha256:[a-f0-9]+)/);
390-
if (digestMatch) {
391-
imageDigest = digestMatch[1];
430+
spinner.text = `Pushing to ECR... ${overallPercent}% (${uploadedMB}/${totalMB} MB, ${layerProgress.size} layers)`;
431+
}
432+
433+
// Check for pushed layers
434+
if (line.includes(': Pushed')) {
435+
completedLayers++;
436+
}
437+
438+
// Check for completion digest
439+
const digestMatch = line.match(/digest: (sha256:[a-f0-9]+)/);
440+
if (digestMatch) {
441+
imageDigest = digestMatch[1];
442+
}
392443
}
393444
}
394-
});
395-
}
445+
} catch {
446+
// Ignore stream errors during processing
447+
}
448+
};
396449

397450
try {
398-
await pushProcess;
451+
// Process stderr in parallel with waiting for exit
452+
await Promise.all([processStderr(), pushProcess.exited]);
453+
454+
const exitCode = pushProcess.exitCode;
399455
const pushTime = Date.now() - startTime;
400456

457+
if (exitCode !== 0) {
458+
spinner.fail('Failed to push image to ECR');
459+
throw new Error('Docker push failed');
460+
}
461+
401462
spinner.succeed(
402463
`Image pushed in ${(pushTime / 1000).toFixed(1)}s (${completedLayers} layers)`
403464
);
@@ -477,8 +538,15 @@ export async function cleanupLocalImages(imageTags: string[]): Promise<void> {
477538
);
478539

479540
try {
480-
await execa('docker', ['rmi', ...imageTags, '--force']);
481-
logger.info({ src: 'cli', util: 'docker-build' }, 'Local images cleaned up');
541+
const result = await bunExec('docker', ['rmi', ...imageTags, '--force']);
542+
if (result.success) {
543+
logger.info({ src: 'cli', util: 'docker-build' }, 'Local images cleaned up');
544+
} else {
545+
logger.warn(
546+
{ src: 'cli', util: 'docker-build', error: result.stderr },
547+
'Failed to clean up some images'
548+
);
549+
}
482550
} catch (error) {
483551
const errorMessage = error instanceof Error ? error.message : String(error);
484552
logger.warn(

packages/cli/src/commands/report/src/__tests__/integration.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
1212
import { promises as fs } from 'fs';
1313
import { join } from 'path';
14-
import { execSync } from 'child_process';
1514
import { tmpdir } from 'os';
1615
import { executeGenerateCommand } from '../../generate';
1716
import { ScenarioRunResult } from '../../../scenario/src/schema';

packages/cli/src/commands/tee/phala-wrapper.ts

Lines changed: 35 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Command } from 'commander';
2-
import { spawn } from 'node:child_process';
32
import { logger } from '@elizaos/core';
43
import { emoji } from '../../utils/emoji-handler';
54

@@ -30,42 +29,45 @@ export const phalaCliCommand = new Command('phala')
3029
logger.info({ src: 'cli', command: 'tee-phala', args }, 'Running Phala CLI command');
3130

3231
// Use npx with --yes flag to auto-install without prompting
33-
// Security fix: Remove shell: true as args are already properly escaped as array
34-
const phalaProcess = spawn('npx', ['--yes', 'phala', ...args], {
35-
stdio: 'inherit',
36-
});
32+
// Using Bun.spawn for process execution per project guidelines
33+
const phalaProcess = Bun.spawn(['npx', '--yes', 'phala', ...args], {
34+
stdio: ['inherit', 'inherit', 'inherit'],
35+
onExit(_proc, exitCode, _signalCode, error) {
36+
if (error) {
37+
const errorMessage = error instanceof Error ? error.message : String(error);
38+
logger.error(
39+
{ src: 'cli', command: 'tee-phala', error: errorMessage, args },
40+
'Failed to execute Phala CLI'
41+
);
3742

38-
phalaProcess.on('error', (err) => {
39-
const error = err as NodeJS.ErrnoException;
40-
logger.error(
41-
{ src: 'cli', command: 'tee-phala', error: error.message, args },
42-
'Failed to execute Phala CLI'
43-
);
43+
// Check if it's an ENOENT-like error (command not found)
44+
if (errorMessage.includes('ENOENT') || errorMessage.includes('not found')) {
45+
console.error(
46+
`\n${emoji.error('Error: npx not found. Please install Node.js and npm:')}`
47+
);
48+
console.error(' Visit https://nodejs.org or use a version manager like nvm');
49+
console.error(
50+
' curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash'
51+
);
52+
} else {
53+
console.error(`\n${emoji.error('Error: Failed to execute Phala CLI')}`);
54+
console.error(' Try running directly: npx phala [args]');
55+
}
56+
process.exit(1);
57+
}
4458

45-
if (error.code === 'ENOENT') {
46-
console.error(
47-
`\n${emoji.error('Error: npx not found. Please install Node.js and npm:')}`
48-
);
49-
console.error(' Visit https://nodejs.org or use a version manager like nvm');
50-
console.error(
51-
' curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash'
52-
);
53-
} else {
54-
console.error(`\n${emoji.error('Error: Failed to execute Phala CLI')}`);
55-
console.error(' Try running directly: npx phala [args]');
56-
}
57-
process.exit(1);
59+
if (exitCode !== 0) {
60+
logger.warn(
61+
{ src: 'cli', command: 'tee-phala', code: exitCode },
62+
'Phala CLI exited with non-zero code'
63+
);
64+
}
65+
process.exit(exitCode || 0);
66+
},
5867
});
5968

60-
phalaProcess.on('exit', (code) => {
61-
if (code !== 0) {
62-
logger.warn(
63-
{ src: 'cli', command: 'tee-phala', code },
64-
'Phala CLI exited with non-zero code'
65-
);
66-
}
67-
process.exit(code || 0);
68-
});
69+
// Wait for the process to complete
70+
await phalaProcess.exited;
6971
} catch (error) {
7072
logger.error(
7173
{

packages/core/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ The correct build is automatically selected based on your environment through pa
4848
npm install buffer crypto-browserify stream-browserify events
4949
```
5050

51-
For more details on the dual build system, see [BUILD_SYSTEM.md](./BUILD_SYSTEM.md).
51+
The dual build system uses conditional exports in package.json to automatically select the appropriate build based on the runtime environment.
5252

5353
## Configuration
5454

0 commit comments

Comments
 (0)