Lentil Hoffman d0941f417e
refactor: optimize lossless view and improve code quality
- Optimize pointer handling and entity reference tracking in Lossless view
- Improve type safety with proper TypeScript types
- Add debug logging for better troubleshooting
- Clean up imports and unused variables
- Update test cases for DeltaBuilder
2025-06-25 11:18:52 -05:00

142 lines
4.5 KiB
TypeScript

import Docker from 'dockerode';
import * as path from 'path';
import { promises as fs } from 'fs';
import * as tar from 'tar-fs';
import { Headers } from 'tar-fs';
import { IImageManager } from './interfaces';
import Debug from 'debug';
const debug = Debug('rz:docker:image-manager');
// Global promise to track test image build
let testImageBuildPromise: Promise<void> | null = null;
export class ImageManager implements IImageManager {
private docker: Docker;
constructor() {
this.docker = new Docker();
}
/**
* Build a test Docker image if it doesn't exist
*/
async buildTestImage(imageName: string = 'rhizome-node-test'): Promise<void> {
if (testImageBuildPromise) {
debug('Test image build in progress, reusing existing build promise...');
return testImageBuildPromise;
}
debug('Building test Docker image...');
const dockerfilePath = path.join(process.cwd(), 'Dockerfile.test');
// Verify Dockerfile exists
try {
await fs.access(dockerfilePath);
debug(`Found Dockerfile at: %s`, dockerfilePath);
} catch (err) {
throw new Error(`Dockerfile not found at ${dockerfilePath}: ${err}`);
}
// Create a tar archive of the build context
const tarStream = tar.pack(process.cwd(), {
entries: [
'Dockerfile.test',
'package.json',
'package-lock.json',
'tsconfig.json',
'src/',
'markdown/',
'util',
'examples/',
'README.md',
],
map: (header: Headers) => {
// Ensure Dockerfile is named 'Dockerfile' in the build context
if (header.name === 'Dockerfile.test') {
header.name = 'Dockerfile';
}
return header;
}
});
debug('Created build context tar stream');
testImageBuildPromise = new Promise<void>((resolve, reject) => {
const log = (...args: unknown[]) => {
const message = args.map(arg =>
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
).join(' ');
debug(message);
};
this.docker.buildImage(tarStream, { t: imageName }, (err, stream) => {
if (err) {
const errorMsg = `❌ Error starting Docker build: ${err.message}`;
log(errorMsg);
return reject(new Error(errorMsg));
}
if (!stream) {
const error = new Error('No build stream returned from Docker');
log(`${error.message}`);
return reject(error);
}
log('✅ Docker build started, streaming output...');
// Handle build output
let output = '';
stream.on('data', (chunk: Buffer) => {
const chunkStr = chunk.toString();
output += chunkStr;
try {
// Try to parse as JSON (Docker build output is typically JSONL)
const lines = chunkStr.split('\n').filter(Boolean);
for (const line of lines) {
try {
if (!line.trim()) continue;
const json = JSON.parse(line);
if (json.stream) {
const message = `[Docker Build] ${json.stream}`.trim();
log(message);
} else if (json.error) {
const errorMsg = json.error.trim() || 'Unknown error during Docker build';
log(`${errorMsg}`);
reject(new Error(errorMsg));
return;
} else if (Object.keys(json).length > 0) {
// Log any other non-empty JSON objects
log(`[Docker Build] ${JSON.stringify(json)}`);
}
} catch (_e) {
// If not JSON, log as plain text if not empty
if (line.trim()) {
log(`[Docker Build] ${line}`);
}
}
}
} catch (e) {
const errorMsg = `Error processing build output: ${e}\nRaw output: ${chunkStr}`;
log(`${errorMsg}`);
debug('Docker build error: %s', errorMsg);
}
});
stream.on('end', () => {
log('✅ Docker build completed successfully');
resolve();
});
stream.on('error', (err: Error) => {
const errorMsg = `❌ Docker build failed: ${err.message}\nBuild output so far: ${output}`;
log(errorMsg);
reject(new Error(errorMsg));
});
});
});
}
}