Skip to content

Commit 693c26a

Browse files
committed
feat(pgpm): add --include flag for additional Docker services (minio)
- Add ServiceDefinition registry for additional services - Add --include <svc> flag to start/stop additional services alongside Postgres - Add 'ls' subcommand to list available services and their status - Add minio as first additional service (port 9000) - Postgres remains the primary service, started by default - All existing commands remain backward compatible
1 parent 79cd3e6 commit 693c26a

2 files changed

Lines changed: 182 additions & 12 deletions

File tree

pgpm/cli/src/commands/docker.ts

Lines changed: 181 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,41 @@ Docker Command:
66
77
pgpm docker <subcommand> [OPTIONS]
88
9-
Manage PostgreSQL Docker containers for local development.
9+
Manage Docker containers for local development.
10+
PostgreSQL is always started by default. Additional services can be
11+
included with the --include flag.
1012
1113
Subcommands:
12-
start Start PostgreSQL container
13-
stop Stop PostgreSQL container
14+
start Start containers
15+
stop Stop containers
16+
ls List available services and their status
1417
15-
Options:
16-
--help, -h Show this help message
18+
PostgreSQL Options:
1719
--name <name> Container name (default: postgres)
1820
--image <image> Docker image (default: constructiveio/postgres-plus:18)
1921
--port <port> Host port mapping (default: 5432)
2022
--user <user> PostgreSQL user (default: postgres)
2123
--password <pass> PostgreSQL password (default: password)
2224
--shm-size <size> Shared memory size for container (default: 2g)
23-
--recreate Remove and recreate container on start
25+
26+
General Options:
27+
--help, -h Show this help message
28+
--recreate Remove and recreate containers on start
29+
--include <svc> Include additional service (can be repeated)
30+
31+
Available Additional Services:
32+
minio MinIO S3-compatible object storage (port 9000)
2433
2534
Examples:
26-
pgpm docker start Start default PostgreSQL container
35+
pgpm docker start Start PostgreSQL only
36+
pgpm docker start --include minio Start PostgreSQL + MinIO
2737
pgpm docker start --port 5433 Start on custom port
2838
pgpm docker start --shm-size 4g Start with 4GB shared memory
29-
pgpm docker start --recreate Remove and recreate container
30-
pgpm docker stop Stop PostgreSQL container
39+
pgpm docker start --recreate Remove and recreate containers
40+
pgpm docker start --recreate --include minio Recreate PostgreSQL + MinIO
41+
pgpm docker stop Stop PostgreSQL
42+
pgpm docker stop --include minio Stop PostgreSQL + MinIO
43+
pgpm docker ls List services and status
3144
`;
3245

3346
interface DockerRunOptions {
@@ -40,6 +53,32 @@ interface DockerRunOptions {
4053
recreate?: boolean;
4154
}
4255

56+
interface PortMapping {
57+
host: number;
58+
container: number;
59+
}
60+
61+
interface ServiceDefinition {
62+
name: string;
63+
image: string;
64+
ports: PortMapping[];
65+
env: Record<string, string>;
66+
command?: string[];
67+
}
68+
69+
const ADDITIONAL_SERVICES: Record<string, ServiceDefinition> = {
70+
minio: {
71+
name: 'minio',
72+
image: 'minio/minio',
73+
ports: [{ host: 9000, container: 9000 }],
74+
env: {
75+
MINIO_ACCESS_KEY: 'minioadmin',
76+
MINIO_SECRET_KEY: 'minioadmin',
77+
},
78+
command: ['server', '/data'],
79+
},
80+
};
81+
4382
interface SpawnResult {
4483
code: number;
4584
stdout: string;
@@ -196,6 +235,125 @@ async function stopContainer(name: string): Promise<void> {
196235
}
197236
}
198237

238+
async function startService(service: ServiceDefinition, recreate: boolean): Promise<void> {
239+
const { name, image, ports, env: serviceEnv, command } = service;
240+
241+
const exists = await containerExists(name);
242+
const running = await isContainerRunning(name);
243+
244+
if (running === true) {
245+
console.log(`✅ Container "${name}" is already running`);
246+
return;
247+
}
248+
249+
if (recreate && exists) {
250+
console.log(`🗑️ Removing existing container "${name}"...`);
251+
const removeResult = await run('docker', ['rm', '-f', name], { stdio: 'inherit' });
252+
if (removeResult.code !== 0) {
253+
await cliExitWithError(`Failed to remove container "${name}"`);
254+
return;
255+
}
256+
}
257+
258+
if (exists && running === false) {
259+
console.log(`🔄 Starting existing container "${name}"...`);
260+
const startResult = await run('docker', ['start', name], { stdio: 'inherit' });
261+
if (startResult.code === 0) {
262+
console.log(`✅ Container "${name}" started successfully`);
263+
} else {
264+
await cliExitWithError(`Failed to start container "${name}"`);
265+
}
266+
return;
267+
}
268+
269+
console.log(`🚀 Creating and starting new container "${name}"...`);
270+
const runArgs = [
271+
'run',
272+
'-d',
273+
'--name', name,
274+
];
275+
276+
for (const [key, value] of Object.entries(serviceEnv)) {
277+
runArgs.push('-e', `${key}=${value}`);
278+
}
279+
280+
for (const portMapping of ports) {
281+
runArgs.push('-p', `${portMapping.host}:${portMapping.container}`);
282+
}
283+
284+
runArgs.push(image);
285+
286+
if (command) {
287+
runArgs.push(...command);
288+
}
289+
290+
const runResult = await run('docker', runArgs, { stdio: 'inherit' });
291+
if (runResult.code === 0) {
292+
console.log(`✅ Container "${name}" created and started successfully`);
293+
const portInfo = ports.map(p => `localhost:${p.host}`).join(', ');
294+
console.log(`📌 ${name} is available at ${portInfo}`);
295+
} else {
296+
const portInfo = ports.map(p => String(p.host)).join(', ');
297+
await cliExitWithError(`Failed to create container "${name}". Check if port ${portInfo} is already in use.`);
298+
}
299+
}
300+
301+
async function stopService(service: ServiceDefinition): Promise<void> {
302+
await stopContainer(service.name);
303+
}
304+
305+
function parseInclude(args: Partial<Record<string, any>>): string[] {
306+
const include = args.include;
307+
if (!include) return [];
308+
if (Array.isArray(include)) return include as string[];
309+
if (typeof include === 'string') return [include];
310+
return [];
311+
}
312+
313+
function resolveIncludedServices(includeNames: string[]): ServiceDefinition[] {
314+
const services: ServiceDefinition[] = [];
315+
for (const name of includeNames) {
316+
const service = ADDITIONAL_SERVICES[name];
317+
if (!service) {
318+
console.warn(`⚠️ Unknown service: "${name}". Available: ${Object.keys(ADDITIONAL_SERVICES).join(', ')}`);
319+
} else {
320+
services.push(service);
321+
}
322+
}
323+
return services;
324+
}
325+
326+
async function listServices(): Promise<void> {
327+
const dockerAvailable = await checkDockerAvailable();
328+
329+
console.log('\nAvailable services:\n');
330+
console.log(' Primary:');
331+
332+
if (dockerAvailable) {
333+
const pgRunning = await isContainerRunning('postgres');
334+
const pgStatus = pgRunning === true ? '\x1b[32mrunning\x1b[0m' : pgRunning === false ? '\x1b[33mstopped\x1b[0m' : '\x1b[90mnot created\x1b[0m';
335+
console.log(` postgres constructiveio/postgres-plus:18 ${pgStatus}`);
336+
} else {
337+
console.log(' postgres constructiveio/postgres-plus:18 \x1b[90m(docker not available)\x1b[0m');
338+
}
339+
340+
console.log('\n Additional (use --include <name>):');
341+
342+
for (const [key, service] of Object.entries(ADDITIONAL_SERVICES)) {
343+
if (dockerAvailable) {
344+
const running = await isContainerRunning(service.name);
345+
const status = running === true ? '\x1b[32mrunning\x1b[0m' : running === false ? '\x1b[33mstopped\x1b[0m' : '\x1b[90mnot created\x1b[0m';
346+
const portInfo = service.ports.map(p => String(p.host)).join(', ');
347+
console.log(` ${key.padEnd(12)}${service.image.padEnd(36)}${status} port ${portInfo}`);
348+
} else {
349+
const portInfo = service.ports.map(p => String(p.host)).join(', ');
350+
console.log(` ${key.padEnd(12)}${service.image.padEnd(36)}\x1b[90m(docker not available)\x1b[0m port ${portInfo}`);
351+
}
352+
}
353+
354+
console.log('');
355+
}
356+
199357
export default async (
200358
argv: Partial<Record<string, any>>,
201359
_prompter: Inquirerer,
@@ -211,7 +369,7 @@ export default async (
211369

212370
if (!subcommand) {
213371
console.log(dockerUsageText);
214-
await cliExitWithError('No subcommand provided. Use "start" or "stop".');
372+
await cliExitWithError('No subcommand provided. Use "start", "stop", or "ls".');
215373
return;
216374
}
217375
const name = (args.name as string) || 'postgres';
@@ -221,18 +379,30 @@ export default async (
221379
const password = (args.password as string) || 'password';
222380
const shmSize = (args['shm-size'] as string) || (args.shmSize as string) || '2g';
223381
const recreate = args.recreate === true;
382+
const includeNames = parseInclude(args);
383+
const includedServices = resolveIncludedServices(includeNames);
224384

225385
switch (subcommand) {
226386
case 'start':
227387
await startContainer({ name, image, port, user, password, shmSize, recreate });
388+
for (const service of includedServices) {
389+
await startService(service, recreate);
390+
}
228391
break;
229392

230393
case 'stop':
231394
await stopContainer(name);
395+
for (const service of includedServices) {
396+
await stopService(service);
397+
}
398+
break;
399+
400+
case 'ls':
401+
await listServices();
232402
break;
233403

234404
default:
235405
console.log(dockerUsageText);
236-
await cliExitWithError(`Unknown subcommand: ${subcommand}. Use "start" or "stop".`);
406+
await cliExitWithError(`Unknown subcommand: ${subcommand}. Use "start", "stop", or "ls".`);
237407
}
238408
};

pgpm/cli/src/utils/display.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const usageText = `
3939
deps Show change dependencies
4040
4141
Development Tools:
42-
docker Manage PostgreSQL Docker containers (start/stop)
42+
docker Manage Docker containers (start/stop/ls, --include for additional services)
4343
env Manage PostgreSQL environment variables
4444
test-packages Run integration tests on workspace packages
4545

0 commit comments

Comments
 (0)