11#!/usr/bin/env tsx
2+ import { writeFileSync } from 'node:fs' ;
3+ import { resolve } from 'node:path' ;
24import { parseArgs } from 'node:util' ;
35
46import { DockerImageNotFoundError } from './docker-image-not-found-error' ;
57import { BASE_PERFORMANCE_PLANS , isValidPerformancePlan } from './performance-plans' ;
8+ import { createServiceStack } from './service-stack' ;
69import type { CloudflaredResult } from './services/cloudflared' ;
710import type { KeycloakResult } from './services/keycloak' ;
811import type { MailpitResult } from './services/mailpit' ;
912import type { NgrokResult } from './services/ngrok' ;
13+ import { services as SERVICE_REGISTRY } from './services/registry' ;
1014import type { TracingResult } from './services/tracing' ;
1115import type { ServiceName } from './services/types' ;
1216import type { VictoriaLogsResult } from './services/victoria-logs' ;
@@ -44,6 +48,8 @@ ${colors.yellow}Usage:${colors.reset}
4448 npm run stack [options]
4549
4650${ colors . yellow } Options:${ colors . reset }
51+ --services-only Start services only (no n8n containers), write .env for local dev
52+ --services <list> Comma-separated services (e.g. postgres,redis,mailpit,proxy,kafka)
4753 --postgres Use PostgreSQL instead of SQLite
4854 --queue Enable queue mode (requires PostgreSQL)
4955 --source-control Enable source control (Git) container for testing
@@ -108,6 +114,11 @@ ${Object.keys(BASE_PERFORMANCE_PLANS)
108114 . map ( ( name ) => ` npm run stack --plan ${ name } ` )
109115 . join ( '\n' ) }
110116
117+ ${ colors . bright } # Services only (local dev — writes .env for pnpm start)${ colors . reset }
118+ pnpm services --services postgres
119+ pnpm services --services postgres,redis
120+ pnpm services --services postgres,mailpit,proxy
121+
111122 ${ colors . bright } # Parallel instances${ colors . reset }
112123 npm run stack --name test-1
113124 npm run stack --name test-2
@@ -127,8 +138,10 @@ async function main() {
127138 args : process . argv . slice ( 2 ) ,
128139 options : {
129140 help : { type : 'boolean' , short : 'h' } ,
141+ 'services-only' : { type : 'boolean' } ,
130142 postgres : { type : 'boolean' } ,
131143 queue : { type : 'boolean' } ,
144+ services : { type : 'string' } ,
132145 'source-control' : { type : 'boolean' } ,
133146 oidc : { type : 'boolean' } ,
134147 observability : { type : 'boolean' } ,
@@ -152,8 +165,20 @@ async function main() {
152165 process . exit ( 0 ) ;
153166 }
154167
168+ const servicesOnly = values [ 'services-only' ] ?? false ;
169+
155170 // Build services array from CLI flags
171+ const validServiceNames = new Set ( Object . keys ( SERVICE_REGISTRY ) ) ;
156172 const services : ServiceName [ ] = [ ] ;
173+ if ( values . services ) {
174+ for ( const name of values . services . split ( ',' ) . map ( ( s ) => s . trim ( ) ) ) {
175+ if ( ! validServiceNames . has ( name ) ) {
176+ log . error ( `Unknown service: '${ name } '. Available: ${ [ ...validServiceNames ] . join ( ', ' ) } ` ) ;
177+ process . exit ( 1 ) ;
178+ }
179+ services . push ( name as ServiceName ) ;
180+ }
181+ }
157182 if ( values [ 'source-control' ] ) services . push ( 'gitea' ) ;
158183 if ( values . oidc ) services . push ( 'keycloak' ) ;
159184 if ( values . observability ) services . push ( 'victoriaLogs' , 'victoriaMetrics' , 'vector' ) ;
@@ -167,7 +192,11 @@ async function main() {
167192 const config : N8NConfig = {
168193 postgres : values . postgres ?? false ,
169194 services,
170- projectName : values . name ?? `n8n-stack-${ Math . random ( ) . toString ( 36 ) . substring ( 7 ) } ` ,
195+ projectName :
196+ values . name ??
197+ ( servicesOnly
198+ ? `n8n-svc-${ Math . random ( ) . toString ( 36 ) . substring ( 7 ) } `
199+ : `n8n-stack-${ Math . random ( ) . toString ( 36 ) . substring ( 7 ) } ` ) ,
171200 } ;
172201
173202 // Handle queue mode (mains > 1 or workers > 0)
@@ -227,6 +256,94 @@ async function main() {
227256 }
228257 }
229258
259+ // Services-only mode: start containers, write .env, no n8n
260+ if ( servicesOnly ) {
261+ if ( services . length === 0 ) {
262+ log . error ( 'No services specified. Use flags like --postgres, --redis, --mailpit, etc.' ) ;
263+ process . exit ( 1 ) ;
264+ }
265+
266+ log . header ( 'Starting service containers' ) ;
267+ log . info ( `Project: ${ config . projectName } ` ) ;
268+ log . info ( `Services: ${ services . join ( ', ' ) } ` ) ;
269+
270+ try {
271+ const stack = await createServiceStack ( {
272+ services,
273+ projectName : config . projectName ,
274+ } ) ;
275+
276+ // Collect host-compatible env vars from each service
277+ const envVars : Record < string , string > = { } ;
278+ for ( const name of services ) {
279+ const result = stack . serviceResults [ name ] ;
280+ if ( ! result ) continue ;
281+
282+ const service = SERVICE_REGISTRY [ name ] ;
283+ Object . assign (
284+ envVars ,
285+ service . env ?.( result , true ) ?? { } ,
286+ service . extraEnv ?.( result , true ) ?? { } ,
287+ ) ;
288+ }
289+
290+ // Write .env to packages/cli/bin/ because `pnpm start` runs os-normalize.mjs
291+ // which does `cd packages/cli/bin` before launching n8n, and dotenv loads from cwd.
292+ if ( Object . keys ( envVars ) . length > 0 ) {
293+ const repoRoot = resolve ( __dirname , '../../..' ) ;
294+ const envPath = resolve ( repoRoot , 'packages/cli/bin/.env' ) ;
295+ const lines = [
296+ '# Generated by pnpm services — do not edit' ,
297+ `# Project: ${ stack . projectName } ` ,
298+ '# Stop with: pnpm --filter n8n-containers services:clean' ,
299+ '' ,
300+ ...Object . entries ( envVars ) . map ( ( [ key , value ] ) => `${ key } =${ value } ` ) ,
301+ '' ,
302+ ] ;
303+ writeFileSync ( envPath , lines . join ( '\n' ) ) ;
304+ log . success ( `Wrote ${ Object . keys ( envVars ) . length } env vars to packages/cli/bin/.env` ) ;
305+ }
306+
307+ // Print summary
308+ log . header ( 'Services running' ) ;
309+ for ( const name of services ) {
310+ const result = stack . serviceResults [ name ] ;
311+ if ( ! result ) continue ;
312+
313+ const service = SERVICE_REGISTRY [ name ] ;
314+ const vars = {
315+ ...( service . env ?.( result , true ) ?? { } ) ,
316+ ...( service . extraEnv ?.( result , true ) ?? { } ) ,
317+ } ;
318+ const varSummary = Object . entries ( vars )
319+ . map ( ( [ k , v ] ) => `${ k } =${ v } ` )
320+ . join ( ', ' ) ;
321+ log . success ( `${ name } ${ varSummary ? `: ${ varSummary } ` : '' } ` ) ;
322+ }
323+
324+ // Print mailpit UI URL if running
325+ const mailpitResult = stack . serviceResults . mailpit as MailpitResult | undefined ;
326+ if ( mailpitResult ) {
327+ console . log ( '' ) ;
328+ log . info ( `Mailpit UI: ${ colors . cyan } ${ mailpitResult . meta . apiBaseUrl } ${ colors . reset } ` ) ;
329+ }
330+
331+ console . log ( '' ) ;
332+ log . info ( 'Containers are running in the background' ) ;
333+ log . info ( `Run ${ colors . bright } pnpm dev${ colors . reset } in another terminal to start n8n` ) ;
334+ log . info (
335+ `Cleanup: ${ colors . bright } pnpm --filter n8n-containers services:clean${ colors . reset } ` ,
336+ ) ;
337+ console . log ( '' ) ;
338+ } catch ( error ) {
339+ log . error (
340+ `Failed to start services: ${ error instanceof Error ? error . message : String ( error ) } ` ,
341+ ) ;
342+ process . exit ( 1 ) ;
343+ }
344+ return ;
345+ }
346+
230347 log . header ( 'Starting n8n Stack' ) ;
231348 log . info ( `Project name: ${ config . projectName } ` ) ;
232349 displayConfig ( config ) ;
0 commit comments