diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b589c7..1435470 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,17 @@ jobs: - name: Run unit tests run: bun run test:unit + - name: Check Docker availability + id: docker-check + run: | + if docker version >/dev/null 2>&1; then + echo "available=true" >> "$GITHUB_OUTPUT" + else + echo "available=false" >> "$GITHUB_OUTPUT" + fi + - name: Run full test suite (Docker) + if: steps.docker-check.outputs.available == 'true' run: bun run test release: @@ -85,3 +95,54 @@ jobs: generate_release_notes: true draft: false prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }} + + publish-npm: + name: Publish to npm + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: release + if: startsWith(github.ref, 'refs/tags/v') + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - uses: actions/setup-node@v4 + with: + node-version: '20' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build packages + run: bun run build + + - name: Publish @ignite/shared + run: npm publish + working-directory: packages/shared + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish @ignite/core + run: npm publish + working-directory: packages/core + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish @ignite/http + run: npm publish + working-directory: packages/http + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Publish @ignite/cli + run: npm publish + working-directory: packages/cli + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/package.json b/package.json index 23fc556..7e3d2f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ignite", - "version": "0.8.0", + "version": "0.9.0", "private": true, "description": "Secure JS/TS code execution in Docker with sandboxing for AI agents, untrusted code, and microservices", "workspaces": [ diff --git a/packages/cli/package.json b/packages/cli/package.json index af59e39..32a3fa9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,11 +1,14 @@ { "name": "@ignite/cli", - "version": "0.8.0", + "version": "0.9.0", "type": "module", "bin": { "ignite": "./dist/index.js" }, "main": "./dist/index.js", + "publishConfig": { + "access": "public" + }, "scripts": { "build": "bun run tsc", "dev": "bun run tsc --watch", diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index 1b4be2c..e0c4fd4 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -10,6 +10,7 @@ interface RunOptions { audit?: boolean; runtime?: string; auditOutput?: string; + stream?: boolean; } export async function runCommand(servicePath: string, options: RunOptions): Promise { @@ -52,7 +53,14 @@ export async function runCommand(servicePath: string, options: RunOptions): Prom ? (await loadPolicyFile(service.servicePath)) ?? DEFAULT_POLICY : undefined; - const metrics = await executeService(service, { input, skipBuild: true, audit: options.audit, policy }); + const metrics = await executeService(service, { + input, + skipBuild: true, + audit: options.audit, + policy, + onStdout: options.stream ? (chunk) => process.stdout.write(chunk) : undefined, + onStderr: options.stream ? (chunk) => process.stderr.write(chunk) : undefined, + }); const report = createReport(preflightResult, metrics); const audit = options.audit && policy diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index e75357d..a35c340 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -13,7 +13,7 @@ const program = new Command(); program .name('ignite') .description('Secure sandbox for AI-generated code, untrusted scripts, and JS/TS execution') - .version('0.8.0'); + .version('0.9.0'); program .command('init ') @@ -31,6 +31,7 @@ program .option('--json', 'Output results as JSON') .option('--audit', 'Run with security audit (blocks network, read-only filesystem)') .option('--audit-output ', 'Write security audit to a JSON file') + .option('--stream', 'Stream container output to the terminal in real time') .action(runCommand); program diff --git a/packages/core/package.json b/packages/core/package.json index 58fb0d6..2f290c9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@ignite/core", - "version": "0.8.0", + "version": "0.9.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -10,6 +10,9 @@ "types": "./dist/index.d.ts" } }, + "publishConfig": { + "access": "public" + }, "scripts": { "build": "bun run tsc", "dev": "bun run tsc --watch", diff --git a/packages/core/src/__tests__/docker-execution.test.ts b/packages/core/src/__tests__/docker-execution.test.ts index 24675e5..3b95ef9 100644 --- a/packages/core/src/__tests__/docker-execution.test.ts +++ b/packages/core/src/__tests__/docker-execution.test.ts @@ -65,6 +65,21 @@ describe('Docker Execution', () => { expect(metrics.stdout).toContain('Ignite'); }, 120000); + it('streams stdout in real time when onStdout callback is provided', async () => { + if (!dockerAvailable) return; + + const service = await loadService(HELLO_BUN_PATH); + const chunks: string[] = []; + + const metrics = await executeService(service, { + onStdout: (chunk) => chunks.push(chunk), + }); + + expect(metrics.exitCode).toBe(0); + expect(chunks.length).toBeGreaterThan(0); + expect(chunks.join('')).toBe(metrics.stdout); + }, 120000); + it('returns correct cold start detection', async () => { if (!dockerAvailable) return; diff --git a/packages/core/src/__tests__/docker-runtime.test.ts b/packages/core/src/__tests__/docker-runtime.test.ts index 7fb1a9d..9c9f523 100644 --- a/packages/core/src/__tests__/docker-runtime.test.ts +++ b/packages/core/src/__tests__/docker-runtime.test.ts @@ -18,4 +18,25 @@ describe('buildDockerRunArgs', () => { expect(args).toContain('--name'); expect(args).toContain('ignite-test-container'); }); + + it('produces the same args regardless of whether streaming callbacks are provided', () => { + const baseOptions = { + imageName: 'ignite-test', + containerName: 'ignite-test-container', + memoryLimitMb: 64, + timeoutMs: 1000, + workDir: '/app', + env: {}, + volumes: [], + }; + + const argsWithoutStreaming = buildDockerRunArgs(baseOptions); + const argsWithStreaming = buildDockerRunArgs({ + ...baseOptions, + onStdout: () => {}, + onStderr: () => {}, + }); + + expect(argsWithStreaming).toEqual(argsWithoutStreaming); + }); }); diff --git a/packages/core/src/execution/execute.ts b/packages/core/src/execution/execute.ts index ada89d4..17f9bee 100644 --- a/packages/core/src/execution/execute.ts +++ b/packages/core/src/execution/execute.ts @@ -17,6 +17,10 @@ export interface ExecuteOptions { skipBuild?: boolean; audit?: boolean; policy?: SecurityPolicy; + /** Called with each stdout chunk as it arrives from the container in real time */ + onStdout?: (chunk: string) => void; + /** Called with each stderr chunk as it arrives from the container in real time */ + onStderr?: (chunk: string) => void; } interface ExecutionState { @@ -76,6 +80,8 @@ export async function executeService( NODE_ENV: 'production', }, security: options.audit ? securityOptions : undefined, + onStdout: options.onStdout, + onStderr: options.onStderr, }); const metrics = parseMetrics(runResult, isColdStart); diff --git a/packages/core/src/runtime/docker-runtime.ts b/packages/core/src/runtime/docker-runtime.ts index e8ae961..493cbbd 100644 --- a/packages/core/src/runtime/docker-runtime.ts +++ b/packages/core/src/runtime/docker-runtime.ts @@ -92,7 +92,7 @@ export async function dockerRun(options: DockerRunOptions): Promise null); @@ -188,7 +188,7 @@ function execDocker(args: string[]): Promise { }); } -function execDockerWithTimeout(args: string[], timeoutMs: number): Promise { +function execDockerWithTimeout(args: string[], timeoutMs: number, onStdout?: (chunk: string) => void, onStderr?: (chunk: string) => void): Promise { return new Promise((resolve) => { const proc = spawn('docker', args, { stdio: ['ignore', 'pipe', 'pipe'] }); @@ -202,11 +202,15 @@ function execDockerWithTimeout(args: string[], timeoutMs: number): Promise { - stdout += data.toString(); + const chunk = data.toString(); + stdout += chunk; + onStdout?.(chunk); }); proc.stderr.on('data', (data: Buffer) => { - stderr += data.toString(); + const chunk = data.toString(); + stderr += chunk; + onStderr?.(chunk); }); proc.on('close', (code) => { diff --git a/packages/core/src/runtime/runtime.types.ts b/packages/core/src/runtime/runtime.types.ts index 9b8aa2f..949097d 100644 --- a/packages/core/src/runtime/runtime.types.ts +++ b/packages/core/src/runtime/runtime.types.ts @@ -16,6 +16,10 @@ export interface DockerRunOptions { env: Record; command?: string[]; security?: SecurityOptions; + /** Called with each stdout chunk as it arrives from the container */ + onStdout?: (chunk: string) => void; + /** Called with each stderr chunk as it arrives from the container */ + onStderr?: (chunk: string) => void; } export interface SecurityOptions { diff --git a/packages/http/package.json b/packages/http/package.json index b172b49..837f29b 100644 --- a/packages/http/package.json +++ b/packages/http/package.json @@ -1,6 +1,6 @@ { "name": "@ignite/http", - "version": "0.8.0", + "version": "0.9.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -10,6 +10,9 @@ "types": "./dist/index.d.ts" } }, + "publishConfig": { + "access": "public" + }, "scripts": { "build": "bun run tsc", "dev": "bun run tsc --watch", diff --git a/packages/runtime-bun/package.json b/packages/runtime-bun/package.json index 05b4c51..7c6995f 100644 --- a/packages/runtime-bun/package.json +++ b/packages/runtime-bun/package.json @@ -1,6 +1,6 @@ { "name": "@ignite/runtime-bun", - "version": "0.8.0", + "version": "0.9.0", "private": true, "description": "Bun runtime adapter for Ignite" } diff --git a/packages/shared/package.json b/packages/shared/package.json index c98c4d6..f45b17b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@ignite/shared", - "version": "0.8.0", + "version": "0.9.0", "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -10,6 +10,9 @@ "types": "./dist/index.d.ts" } }, + "publishConfig": { + "access": "public" + }, "scripts": { "build": "bun run tsc", "dev": "bun run tsc --watch",