Skip to content

Commit 9668394

Browse files
Mossakaclaude
andcommitted
feat(cli): add predownload command to pre-pull container images
Adds `awf predownload` subcommand that pulls Docker images ahead of time for offline use or faster startup. Supports --image-registry, --image-tag, --agent-image, and --enable-api-proxy flags. After predownloading, users can use --skip-pull to avoid pulling at runtime. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3cda474 commit 9668394

File tree

3 files changed

+149
-0
lines changed

3 files changed

+149
-0
lines changed

src/cli.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,32 @@ export function validateFormat(format: string, validFormats: string[]): void {
14821482
}
14831483
}
14841484

1485+
// Predownload subcommand - pre-pull container images
1486+
program
1487+
.command('predownload')
1488+
.description('Pre-download Docker images for offline use or faster startup')
1489+
.option(
1490+
'--image-registry <registry>',
1491+
'Container image registry',
1492+
'ghcr.io/github/gh-aw-firewall'
1493+
)
1494+
.option('--image-tag <tag>', 'Container image tag', 'latest')
1495+
.option(
1496+
'--agent-image <value>',
1497+
'Agent image preset (default, act) or custom image',
1498+
'default'
1499+
)
1500+
.option('--enable-api-proxy', 'Also download the API proxy image', false)
1501+
.action(async (options) => {
1502+
const { predownloadCommand } = await import('./commands/predownload');
1503+
await predownloadCommand({
1504+
imageRegistry: options.imageRegistry,
1505+
imageTag: options.imageTag,
1506+
agentImage: options.agentImage,
1507+
enableApiProxy: options.enableApiProxy,
1508+
});
1509+
});
1510+
14851511
// Logs subcommand - view Squid proxy logs
14861512
const logsCmd = program
14871513
.command('logs')

src/commands/predownload.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { resolveImages, PredownloadOptions } from './predownload';
2+
3+
describe('predownload', () => {
4+
describe('resolveImages', () => {
5+
const defaults: PredownloadOptions = {
6+
imageRegistry: 'ghcr.io/github/gh-aw-firewall',
7+
imageTag: 'latest',
8+
agentImage: 'default',
9+
enableApiProxy: false,
10+
};
11+
12+
it('should resolve squid and default agent images', () => {
13+
const images = resolveImages(defaults);
14+
expect(images).toEqual([
15+
'ghcr.io/github/gh-aw-firewall/squid:latest',
16+
'ghcr.io/github/gh-aw-firewall/agent:latest',
17+
]);
18+
});
19+
20+
it('should resolve agent-act image for act preset', () => {
21+
const images = resolveImages({ ...defaults, agentImage: 'act' });
22+
expect(images).toEqual([
23+
'ghcr.io/github/gh-aw-firewall/squid:latest',
24+
'ghcr.io/github/gh-aw-firewall/agent-act:latest',
25+
]);
26+
});
27+
28+
it('should include api-proxy when enabled', () => {
29+
const images = resolveImages({ ...defaults, enableApiProxy: true });
30+
expect(images).toEqual([
31+
'ghcr.io/github/gh-aw-firewall/squid:latest',
32+
'ghcr.io/github/gh-aw-firewall/agent:latest',
33+
'ghcr.io/github/gh-aw-firewall/api-proxy:latest',
34+
]);
35+
});
36+
37+
it('should use custom registry and tag', () => {
38+
const images = resolveImages({
39+
...defaults,
40+
imageRegistry: 'my-registry.io/awf',
41+
imageTag: 'v1.0.0',
42+
});
43+
expect(images).toEqual([
44+
'my-registry.io/awf/squid:v1.0.0',
45+
'my-registry.io/awf/agent:v1.0.0',
46+
]);
47+
});
48+
49+
it('should use custom agent image as-is', () => {
50+
const images = resolveImages({ ...defaults, agentImage: 'ubuntu:22.04' });
51+
expect(images).toEqual([
52+
'ghcr.io/github/gh-aw-firewall/squid:latest',
53+
'ubuntu:22.04',
54+
]);
55+
});
56+
});
57+
});

src/commands/predownload.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import execa from 'execa';
2+
import { logger } from '../logger';
3+
4+
export interface PredownloadOptions {
5+
imageRegistry: string;
6+
imageTag: string;
7+
agentImage: string;
8+
enableApiProxy: boolean;
9+
}
10+
11+
/**
12+
* Resolves the list of image references to pull based on the given options.
13+
*/
14+
export function resolveImages(options: PredownloadOptions): string[] {
15+
const { imageRegistry, imageTag, agentImage, enableApiProxy } = options;
16+
const images: string[] = [];
17+
18+
// Always pull squid
19+
images.push(`${imageRegistry}/squid:${imageTag}`);
20+
21+
// Pull agent image based on preset
22+
const isPreset = agentImage === 'default' || agentImage === 'act';
23+
if (isPreset) {
24+
const imageName = agentImage === 'act' ? 'agent-act' : 'agent';
25+
images.push(`${imageRegistry}/${imageName}:${imageTag}`);
26+
} else {
27+
// Custom image - pull as-is
28+
images.push(agentImage);
29+
}
30+
31+
// Optionally pull api-proxy
32+
if (enableApiProxy) {
33+
images.push(`${imageRegistry}/api-proxy:${imageTag}`);
34+
}
35+
36+
return images;
37+
}
38+
39+
/**
40+
* Pre-download Docker images for offline use or faster startup.
41+
*/
42+
export async function predownloadCommand(options: PredownloadOptions): Promise<void> {
43+
const images = resolveImages(options);
44+
45+
logger.info(`Pre-downloading ${images.length} image(s)...`);
46+
47+
let failed = 0;
48+
for (const image of images) {
49+
logger.info(`Pulling ${image}...`);
50+
try {
51+
await execa('docker', ['pull', image], { stdio: 'inherit' });
52+
logger.info(`Successfully pulled ${image}`);
53+
} catch (error) {
54+
logger.error(`Failed to pull ${image}: ${error instanceof Error ? error.message : error}`);
55+
failed++;
56+
}
57+
}
58+
59+
if (failed > 0) {
60+
logger.error(`${failed} of ${images.length} image(s) failed to pull`);
61+
process.exit(1);
62+
}
63+
64+
logger.info(`All ${images.length} image(s) pre-downloaded successfully`);
65+
logger.info('You can now use --skip-pull to skip pulling images at runtime');
66+
}

0 commit comments

Comments
 (0)