@git.zone/tsdocker 1.12.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -151,7 +151,7 @@ export class Dockerfile {
151
151
  }
152
152
 
153
153
  /** Starts a temporary registry:2 container on port 5234. */
154
- public static async startLocalRegistry(): Promise<void> {
154
+ public static async startLocalRegistry(isRootless?: boolean): Promise<void> {
155
155
  await smartshellInstance.execSilent(
156
156
  `docker rm -f ${LOCAL_REGISTRY_CONTAINER} 2>/dev/null || true`
157
157
  );
@@ -164,6 +164,9 @@ export class Dockerfile {
164
164
  // registry:2 starts near-instantly; brief wait for readiness
165
165
  await new Promise(resolve => setTimeout(resolve, 1000));
166
166
  logger.log('info', `Started local registry at ${LOCAL_REGISTRY_HOST} (buildx dependency bridge)`);
167
+ if (isRootless) {
168
+ logger.log('warn', `[rootless] Registry on port ${LOCAL_REGISTRY_PORT} — if buildx cannot reach localhost:${LOCAL_REGISTRY_PORT}, try 127.0.0.1:${LOCAL_REGISTRY_PORT}`);
169
+ }
167
170
  }
168
171
 
169
172
  /** Stops and removes the temporary local registry container. */
@@ -186,45 +189,142 @@ export class Dockerfile {
186
189
  logger.log('info', `Pushed ${dockerfile.buildTag} to local registry as ${registryTag}`);
187
190
  }
188
191
 
192
+ /**
193
+ * Groups topologically sorted Dockerfiles into dependency levels.
194
+ * Level 0 = no local dependencies; level N = depends on something in level N-1.
195
+ * Images within the same level are independent and can build in parallel.
196
+ */
197
+ public static computeLevels(sortedDockerfiles: Dockerfile[]): Dockerfile[][] {
198
+ const levelMap = new Map<Dockerfile, number>();
199
+ for (const df of sortedDockerfiles) {
200
+ if (!df.localBaseImageDependent || !df.localBaseDockerfile) {
201
+ levelMap.set(df, 0);
202
+ } else {
203
+ const depLevel = levelMap.get(df.localBaseDockerfile) ?? 0;
204
+ levelMap.set(df, depLevel + 1);
205
+ }
206
+ }
207
+ const maxLevel = Math.max(...Array.from(levelMap.values()), 0);
208
+ const levels: Dockerfile[][] = [];
209
+ for (let l = 0; l <= maxLevel; l++) {
210
+ levels.push(sortedDockerfiles.filter(df => levelMap.get(df) === l));
211
+ }
212
+ return levels;
213
+ }
214
+
215
+ /**
216
+ * Runs async tasks with bounded concurrency (worker-pool pattern).
217
+ * Fast-fail: if any task throws, Promise.all rejects immediately.
218
+ */
219
+ public static async runWithConcurrency<T>(
220
+ tasks: (() => Promise<T>)[],
221
+ concurrency: number,
222
+ ): Promise<T[]> {
223
+ const results: T[] = new Array(tasks.length);
224
+ let nextIndex = 0;
225
+ async function worker(): Promise<void> {
226
+ while (true) {
227
+ const idx = nextIndex++;
228
+ if (idx >= tasks.length) break;
229
+ results[idx] = await tasks[idx]();
230
+ }
231
+ }
232
+ const workers = Array.from(
233
+ { length: Math.min(concurrency, tasks.length) },
234
+ () => worker(),
235
+ );
236
+ await Promise.all(workers);
237
+ return results;
238
+ }
239
+
189
240
  /**
190
241
  * Builds the corresponding real docker image for each Dockerfile class instance
191
242
  */
192
243
  public static async buildDockerfiles(
193
244
  sortedArrayArg: Dockerfile[],
194
- options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean },
245
+ options?: { platform?: string; timeout?: number; noCache?: boolean; verbose?: boolean; isRootless?: boolean; parallel?: boolean; parallelConcurrency?: number },
195
246
  ): Promise<Dockerfile[]> {
196
247
  const total = sortedArrayArg.length;
197
248
  const overallStart = Date.now();
198
249
  const useRegistry = Dockerfile.needsLocalRegistry(sortedArrayArg, options);
199
250
 
200
251
  if (useRegistry) {
201
- await Dockerfile.startLocalRegistry();
252
+ await Dockerfile.startLocalRegistry(options?.isRootless);
202
253
  }
203
254
 
204
255
  try {
205
- for (let i = 0; i < total; i++) {
206
- const dockerfileArg = sortedArrayArg[i];
207
- const progress = `(${i + 1}/${total})`;
208
- logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
209
-
210
- const elapsed = await dockerfileArg.build(options);
211
- logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
212
-
213
- // Tag in host daemon for standard docker build compatibility
214
- const dependentBaseImages = new Set<string>();
215
- for (const other of sortedArrayArg) {
216
- if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
217
- dependentBaseImages.add(other.baseImage);
218
- }
256
+ if (options?.parallel) {
257
+ // === PARALLEL MODE: build independent images concurrently within each level ===
258
+ const concurrency = options.parallelConcurrency ?? 4;
259
+ const levels = Dockerfile.computeLevels(sortedArrayArg);
260
+
261
+ logger.log('info', `Parallel build: ${levels.length} level(s), concurrency ${concurrency}`);
262
+ for (let l = 0; l < levels.length; l++) {
263
+ const level = levels[l];
264
+ logger.log('info', ` Level ${l} (${level.length}): ${level.map(df => df.cleanTag).join(', ')}`);
219
265
  }
220
- for (const fullTag of dependentBaseImages) {
221
- logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
222
- await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
266
+
267
+ let built = 0;
268
+ for (let l = 0; l < levels.length; l++) {
269
+ const level = levels[l];
270
+ logger.log('info', `--- Level ${l}: building ${level.length} image(s) in parallel ---`);
271
+
272
+ const tasks = level.map((df) => {
273
+ const myIndex = ++built;
274
+ return async () => {
275
+ const progress = `(${myIndex}/${total})`;
276
+ logger.log('info', `${progress} Building ${df.cleanTag}...`);
277
+ const elapsed = await df.build(options);
278
+ logger.log('ok', `${progress} Built ${df.cleanTag} in ${formatDuration(elapsed)}`);
279
+ return df;
280
+ };
281
+ });
282
+
283
+ await Dockerfile.runWithConcurrency(tasks, concurrency);
284
+
285
+ // After the entire level completes, tag + push for dependency resolution
286
+ for (const df of level) {
287
+ const dependentBaseImages = new Set<string>();
288
+ for (const other of sortedArrayArg) {
289
+ if (other.localBaseDockerfile === df && other.baseImage !== df.buildTag) {
290
+ dependentBaseImages.add(other.baseImage);
291
+ }
292
+ }
293
+ for (const fullTag of dependentBaseImages) {
294
+ logger.log('info', `Tagging ${df.buildTag} as ${fullTag} for local dependency resolution`);
295
+ await smartshellInstance.exec(`docker tag ${df.buildTag} ${fullTag}`);
296
+ }
297
+ if (useRegistry && sortedArrayArg.some(other => other.localBaseDockerfile === df)) {
298
+ await Dockerfile.pushToLocalRegistry(df);
299
+ }
300
+ }
223
301
  }
302
+ } else {
303
+ // === SEQUENTIAL MODE: build one at a time ===
304
+ for (let i = 0; i < total; i++) {
305
+ const dockerfileArg = sortedArrayArg[i];
306
+ const progress = `(${i + 1}/${total})`;
307
+ logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
308
+
309
+ const elapsed = await dockerfileArg.build(options);
310
+ logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
311
+
312
+ // Tag in host daemon for standard docker build compatibility
313
+ const dependentBaseImages = new Set<string>();
314
+ for (const other of sortedArrayArg) {
315
+ if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
316
+ dependentBaseImages.add(other.baseImage);
317
+ }
318
+ }
319
+ for (const fullTag of dependentBaseImages) {
320
+ logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
321
+ await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
322
+ }
224
323
 
225
- // Push to local registry for buildx dependency resolution
226
- if (useRegistry && sortedArrayArg.some(other => other.localBaseDockerfile === dockerfileArg)) {
227
- await Dockerfile.pushToLocalRegistry(dockerfileArg);
324
+ // Push to local registry for buildx dependency resolution
325
+ if (useRegistry && sortedArrayArg.some(other => other.localBaseDockerfile === dockerfileArg)) {
326
+ await Dockerfile.pushToLocalRegistry(dockerfileArg);
327
+ }
228
328
  }
229
329
  }
230
330
  } finally {
@@ -5,6 +5,7 @@ import { Dockerfile } from './classes.dockerfile.js';
5
5
  import { DockerRegistry } from './classes.dockerregistry.js';
6
6
  import { RegistryStorage } from './classes.registrystorage.js';
7
7
  import { TsDockerCache } from './classes.tsdockercache.js';
8
+ import { DockerContext } from './classes.dockercontext.js';
8
9
  import type { ITsDockerConfig, IBuildCommandOptions } from './interfaces/index.js';
9
10
 
10
11
  const smartshellInstance = new plugins.smartshell.Smartshell({
@@ -18,17 +19,27 @@ export class TsDockerManager {
18
19
  public registryStorage: RegistryStorage;
19
20
  public config: ITsDockerConfig;
20
21
  public projectInfo: any;
22
+ public dockerContext: DockerContext;
21
23
  private dockerfiles: Dockerfile[] = [];
22
24
 
23
25
  constructor(config: ITsDockerConfig) {
24
26
  this.config = config;
25
27
  this.registryStorage = new RegistryStorage();
28
+ this.dockerContext = new DockerContext();
26
29
  }
27
30
 
28
31
  /**
29
32
  * Prepares the manager by loading project info and registries
30
33
  */
31
- public async prepare(): Promise<void> {
34
+ public async prepare(contextArg?: string): Promise<void> {
35
+ // Detect Docker context
36
+ if (contextArg) {
37
+ this.dockerContext.setContext(contextArg);
38
+ }
39
+ await this.dockerContext.detect();
40
+ this.dockerContext.logContextInfo();
41
+ this.dockerContext.logRootlessWarnings();
42
+
32
43
  // Load project info
33
44
  try {
34
45
  const projectinfoInstance = new plugins.projectinfo.ProjectInfo(paths.cwd);
@@ -156,6 +167,16 @@ export class TsDockerManager {
156
167
  logger.log('info', 'Cache: disabled (--no-cache)');
157
168
  }
158
169
 
170
+ if (options?.parallel) {
171
+ const concurrency = options.parallelConcurrency ?? 4;
172
+ const levels = Dockerfile.computeLevels(toBuild);
173
+ logger.log('info', `Parallel build: ${levels.length} level(s), concurrency ${concurrency}`);
174
+ for (let l = 0; l < levels.length; l++) {
175
+ const level = levels[l];
176
+ logger.log('info', ` Level ${l} (${level.length}): ${level.map(df => df.cleanTag).join(', ')}`);
177
+ }
178
+ }
179
+
159
180
  logger.log('info', `Building ${toBuild.length} Dockerfile(s)...`);
160
181
 
161
182
  if (options?.cached) {
@@ -169,45 +190,101 @@ export class TsDockerManager {
169
190
  const useRegistry = Dockerfile.needsLocalRegistry(toBuild, options);
170
191
 
171
192
  if (useRegistry) {
172
- await Dockerfile.startLocalRegistry();
193
+ await Dockerfile.startLocalRegistry(this.dockerContext.contextInfo?.isRootless);
173
194
  }
174
195
 
175
196
  try {
176
- for (let i = 0; i < total; i++) {
177
- const dockerfileArg = toBuild[i];
178
- const progress = `(${i + 1}/${total})`;
179
- const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content);
180
-
181
- if (skip) {
182
- logger.log('ok', `${progress} Skipped ${dockerfileArg.cleanTag} (cached)`);
183
- } else {
184
- logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
185
- const elapsed = await dockerfileArg.build({
186
- platform: options?.platform,
187
- timeout: options?.timeout,
188
- noCache: options?.noCache,
189
- verbose: options?.verbose,
197
+ if (options?.parallel) {
198
+ // === PARALLEL CACHED MODE ===
199
+ const concurrency = options.parallelConcurrency ?? 4;
200
+ const levels = Dockerfile.computeLevels(toBuild);
201
+
202
+ let built = 0;
203
+ for (let l = 0; l < levels.length; l++) {
204
+ const level = levels[l];
205
+ logger.log('info', `--- Level ${l}: building ${level.length} image(s) in parallel ---`);
206
+
207
+ const tasks = level.map((df) => {
208
+ const myIndex = ++built;
209
+ return async () => {
210
+ const progress = `(${myIndex}/${total})`;
211
+ const skip = await cache.shouldSkipBuild(df.cleanTag, df.content);
212
+
213
+ if (skip) {
214
+ logger.log('ok', `${progress} Skipped ${df.cleanTag} (cached)`);
215
+ } else {
216
+ logger.log('info', `${progress} Building ${df.cleanTag}...`);
217
+ const elapsed = await df.build({
218
+ platform: options?.platform,
219
+ timeout: options?.timeout,
220
+ noCache: options?.noCache,
221
+ verbose: options?.verbose,
222
+ });
223
+ logger.log('ok', `${progress} Built ${df.cleanTag} in ${formatDuration(elapsed)}`);
224
+ const imageId = await df.getId();
225
+ cache.recordBuild(df.cleanTag, df.content, imageId, df.buildTag);
226
+ }
227
+ return df;
228
+ };
190
229
  });
191
- logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
192
- const imageId = await dockerfileArg.getId();
193
- cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag);
194
- }
195
230
 
196
- // Tag for dependents IMMEDIATELY (not after all builds)
197
- const dependentBaseImages = new Set<string>();
198
- for (const other of toBuild) {
199
- if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
200
- dependentBaseImages.add(other.baseImage);
231
+ await Dockerfile.runWithConcurrency(tasks, concurrency);
232
+
233
+ // After the entire level completes, tag + push for dependency resolution
234
+ for (const df of level) {
235
+ const dependentBaseImages = new Set<string>();
236
+ for (const other of toBuild) {
237
+ if (other.localBaseDockerfile === df && other.baseImage !== df.buildTag) {
238
+ dependentBaseImages.add(other.baseImage);
239
+ }
240
+ }
241
+ for (const fullTag of dependentBaseImages) {
242
+ logger.log('info', `Tagging ${df.buildTag} as ${fullTag} for local dependency resolution`);
243
+ await smartshellInstance.exec(`docker tag ${df.buildTag} ${fullTag}`);
244
+ }
245
+ if (useRegistry && toBuild.some(other => other.localBaseDockerfile === df)) {
246
+ await Dockerfile.pushToLocalRegistry(df);
247
+ }
201
248
  }
202
249
  }
203
- for (const fullTag of dependentBaseImages) {
204
- logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
205
- await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
206
- }
250
+ } else {
251
+ // === SEQUENTIAL CACHED MODE ===
252
+ for (let i = 0; i < total; i++) {
253
+ const dockerfileArg = toBuild[i];
254
+ const progress = `(${i + 1}/${total})`;
255
+ const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content);
256
+
257
+ if (skip) {
258
+ logger.log('ok', `${progress} Skipped ${dockerfileArg.cleanTag} (cached)`);
259
+ } else {
260
+ logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
261
+ const elapsed = await dockerfileArg.build({
262
+ platform: options?.platform,
263
+ timeout: options?.timeout,
264
+ noCache: options?.noCache,
265
+ verbose: options?.verbose,
266
+ });
267
+ logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
268
+ const imageId = await dockerfileArg.getId();
269
+ cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag);
270
+ }
271
+
272
+ // Tag for dependents IMMEDIATELY (not after all builds)
273
+ const dependentBaseImages = new Set<string>();
274
+ for (const other of toBuild) {
275
+ if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
276
+ dependentBaseImages.add(other.baseImage);
277
+ }
278
+ }
279
+ for (const fullTag of dependentBaseImages) {
280
+ logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
281
+ await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
282
+ }
207
283
 
208
- // Push to local registry for buildx (even for cache hits — image exists but registry doesn't)
209
- if (useRegistry && toBuild.some(other => other.localBaseDockerfile === dockerfileArg)) {
210
- await Dockerfile.pushToLocalRegistry(dockerfileArg);
284
+ // Push to local registry for buildx (even for cache hits — image exists but registry doesn't)
285
+ if (useRegistry && toBuild.some(other => other.localBaseDockerfile === dockerfileArg)) {
286
+ await Dockerfile.pushToLocalRegistry(dockerfileArg);
287
+ }
211
288
  }
212
289
  }
213
290
  } finally {
@@ -225,6 +302,9 @@ export class TsDockerManager {
225
302
  timeout: options?.timeout,
226
303
  noCache: options?.noCache,
227
304
  verbose: options?.verbose,
305
+ isRootless: this.dockerContext.contextInfo?.isRootless,
306
+ parallel: options?.parallel,
307
+ parallelConcurrency: options?.parallelConcurrency,
228
308
  });
229
309
  }
230
310
 
@@ -254,30 +334,32 @@ export class TsDockerManager {
254
334
  * Ensures Docker buildx is set up for multi-architecture builds
255
335
  */
256
336
  private async ensureBuildx(): Promise<void> {
337
+ const builderName = this.dockerContext.getBuilderName();
257
338
  const platforms = this.config.platforms?.join(', ') || 'default';
258
- logger.log('info', `Setting up Docker buildx for multi-platform builds [${platforms}]...`);
259
- const inspectResult = await smartshellInstance.exec('docker buildx inspect tsdocker-builder 2>/dev/null');
339
+ logger.log('info', `Setting up Docker buildx [${platforms}]...`);
340
+ logger.log('info', `Builder: ${builderName}`);
341
+ const inspectResult = await smartshellInstance.exec(`docker buildx inspect ${builderName} 2>/dev/null`);
260
342
 
261
343
  if (inspectResult.exitCode !== 0) {
262
344
  logger.log('info', 'Creating new buildx builder with host network...');
263
345
  await smartshellInstance.exec(
264
- 'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --use'
346
+ `docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host --use`
265
347
  );
266
348
  await smartshellInstance.exec('docker buildx inspect --bootstrap');
267
349
  } else {
268
350
  const inspectOutput = inspectResult.stdout || '';
269
351
  if (!inspectOutput.includes('network=host')) {
270
352
  logger.log('info', 'Recreating buildx builder with host network (migration)...');
271
- await smartshellInstance.exec('docker buildx rm tsdocker-builder 2>/dev/null');
353
+ await smartshellInstance.exec(`docker buildx rm ${builderName} 2>/dev/null`);
272
354
  await smartshellInstance.exec(
273
- 'docker buildx create --name tsdocker-builder --driver docker-container --driver-opt network=host --use'
355
+ `docker buildx create --name ${builderName} --driver docker-container --driver-opt network=host --use`
274
356
  );
275
357
  await smartshellInstance.exec('docker buildx inspect --bootstrap');
276
358
  } else {
277
- await smartshellInstance.exec('docker buildx use tsdocker-builder');
359
+ await smartshellInstance.exec(`docker buildx use ${builderName}`);
278
360
  }
279
361
  }
280
- logger.log('ok', `Docker buildx ready (platforms: ${platforms})`);
362
+ logger.log('ok', `Docker buildx ready (builder: ${builderName}, platforms: ${platforms})`);
281
363
  }
282
364
 
283
365
  /**
@@ -79,6 +79,9 @@ export interface IBuildCommandOptions {
79
79
  noCache?: boolean; // Force rebuild without Docker layer cache (--no-cache)
80
80
  cached?: boolean; // Skip builds when Dockerfile content hasn't changed
81
81
  verbose?: boolean; // Stream raw docker build output (default: silent)
82
+ context?: string; // Explicit Docker context name (--context flag)
83
+ parallel?: boolean; // Enable parallel builds within dependency levels
84
+ parallelConcurrency?: number; // Max concurrent builds per level (default 4)
82
85
  }
83
86
 
84
87
  export interface ICacheEntry {
@@ -92,3 +95,10 @@ export interface ICacheData {
92
95
  version: 1;
93
96
  entries: { [cleanTag: string]: ICacheEntry };
94
97
  }
98
+
99
+ export interface IDockerContextInfo {
100
+ name: string; // 'default', 'rootless', 'colima', etc.
101
+ endpoint: string; // 'unix:///var/run/docker.sock'
102
+ isRootless: boolean;
103
+ dockerHost?: string; // value of DOCKER_HOST env var, if set
104
+ }
@@ -33,7 +33,7 @@ export let run = () => {
33
33
  try {
34
34
  const config = await ConfigModule.run();
35
35
  const manager = new TsDockerManager(config);
36
- await manager.prepare();
36
+ await manager.prepare(argvArg.context as string | undefined);
37
37
 
38
38
  const buildOptions: IBuildCommandOptions = {};
39
39
  const patterns = argvArg._.slice(1) as string[];
@@ -55,6 +55,12 @@ export let run = () => {
55
55
  if (argvArg.verbose) {
56
56
  buildOptions.verbose = true;
57
57
  }
58
+ if (argvArg.parallel) {
59
+ buildOptions.parallel = true;
60
+ if (typeof argvArg.parallel === 'number') {
61
+ buildOptions.parallelConcurrency = argvArg.parallel;
62
+ }
63
+ }
58
64
 
59
65
  await manager.build(buildOptions);
60
66
  logger.log('success', 'Build completed successfully');
@@ -72,7 +78,7 @@ export let run = () => {
72
78
  try {
73
79
  const config = await ConfigModule.run();
74
80
  const manager = new TsDockerManager(config);
75
- await manager.prepare();
81
+ await manager.prepare(argvArg.context as string | undefined);
76
82
 
77
83
  // Login first
78
84
  await manager.login();
@@ -95,6 +101,12 @@ export let run = () => {
95
101
  if (argvArg.verbose) {
96
102
  buildOptions.verbose = true;
97
103
  }
104
+ if (argvArg.parallel) {
105
+ buildOptions.parallel = true;
106
+ if (typeof argvArg.parallel === 'number') {
107
+ buildOptions.parallelConcurrency = argvArg.parallel;
108
+ }
109
+ }
98
110
 
99
111
  // Build images first (if not already built)
100
112
  await manager.build(buildOptions);
@@ -124,7 +136,7 @@ export let run = () => {
124
136
 
125
137
  const config = await ConfigModule.run();
126
138
  const manager = new TsDockerManager(config);
127
- await manager.prepare();
139
+ await manager.prepare(argvArg.context as string | undefined);
128
140
 
129
141
  // Login first
130
142
  await manager.login();
@@ -144,7 +156,7 @@ export let run = () => {
144
156
  try {
145
157
  const config = await ConfigModule.run();
146
158
  const manager = new TsDockerManager(config);
147
- await manager.prepare();
159
+ await manager.prepare(argvArg.context as string | undefined);
148
160
 
149
161
  // Build images first
150
162
  const buildOptions: IBuildCommandOptions = {};
@@ -157,6 +169,12 @@ export let run = () => {
157
169
  if (argvArg.verbose) {
158
170
  buildOptions.verbose = true;
159
171
  }
172
+ if (argvArg.parallel) {
173
+ buildOptions.parallel = true;
174
+ if (typeof argvArg.parallel === 'number') {
175
+ buildOptions.parallelConcurrency = argvArg.parallel;
176
+ }
177
+ }
160
178
  await manager.build(buildOptions);
161
179
 
162
180
  // Run tests
@@ -175,7 +193,7 @@ export let run = () => {
175
193
  try {
176
194
  const config = await ConfigModule.run();
177
195
  const manager = new TsDockerManager(config);
178
- await manager.prepare();
196
+ await manager.prepare(argvArg.context as string | undefined);
179
197
  await manager.login();
180
198
  logger.log('success', 'Login completed successfully');
181
199
  } catch (err) {
@@ -191,7 +209,7 @@ export let run = () => {
191
209
  try {
192
210
  const config = await ConfigModule.run();
193
211
  const manager = new TsDockerManager(config);
194
- await manager.prepare();
212
+ await manager.prepare(argvArg.context as string | undefined);
195
213
  await manager.list();
196
214
  } catch (err) {
197
215
  logger.log('error', `List failed: ${(err as Error).message}`);