@git.zone/tsdocker 1.13.0 → 1.15.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.
@@ -167,6 +167,16 @@ export class TsDockerManager {
167
167
  logger.log('info', 'Cache: disabled (--no-cache)');
168
168
  }
169
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
+
170
180
  logger.log('info', `Building ${toBuild.length} Dockerfile(s)...`);
171
181
 
172
182
  if (options?.cached) {
@@ -184,41 +194,97 @@ export class TsDockerManager {
184
194
  }
185
195
 
186
196
  try {
187
- for (let i = 0; i < total; i++) {
188
- const dockerfileArg = toBuild[i];
189
- const progress = `(${i + 1}/${total})`;
190
- const skip = await cache.shouldSkipBuild(dockerfileArg.cleanTag, dockerfileArg.content);
191
-
192
- if (skip) {
193
- logger.log('ok', `${progress} Skipped ${dockerfileArg.cleanTag} (cached)`);
194
- } else {
195
- logger.log('info', `${progress} Building ${dockerfileArg.cleanTag}...`);
196
- const elapsed = await dockerfileArg.build({
197
- platform: options?.platform,
198
- timeout: options?.timeout,
199
- noCache: options?.noCache,
200
- 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
+ };
201
229
  });
202
- logger.log('ok', `${progress} Built ${dockerfileArg.cleanTag} in ${formatDuration(elapsed)}`);
203
- const imageId = await dockerfileArg.getId();
204
- cache.recordBuild(dockerfileArg.cleanTag, dockerfileArg.content, imageId, dockerfileArg.buildTag);
205
- }
206
230
 
207
- // Tag for dependents IMMEDIATELY (not after all builds)
208
- const dependentBaseImages = new Set<string>();
209
- for (const other of toBuild) {
210
- if (other.localBaseDockerfile === dockerfileArg && other.baseImage !== dockerfileArg.buildTag) {
211
- 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
+ }
212
248
  }
213
249
  }
214
- for (const fullTag of dependentBaseImages) {
215
- logger.log('info', `Tagging ${dockerfileArg.buildTag} as ${fullTag} for local dependency resolution`);
216
- await smartshellInstance.exec(`docker tag ${dockerfileArg.buildTag} ${fullTag}`);
217
- }
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
+ }
218
271
 
219
- // Push to local registry for buildx (even for cache hits — image exists but registry doesn't)
220
- if (useRegistry && toBuild.some(other => other.localBaseDockerfile === dockerfileArg)) {
221
- await Dockerfile.pushToLocalRegistry(dockerfileArg);
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
+ }
283
+
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
+ }
222
288
  }
223
289
  }
224
290
  } finally {
@@ -237,6 +303,8 @@ export class TsDockerManager {
237
303
  noCache: options?.noCache,
238
304
  verbose: options?.verbose,
239
305
  isRootless: this.dockerContext.contextInfo?.isRootless,
306
+ parallel: options?.parallel,
307
+ parallelConcurrency: options?.parallelConcurrency,
240
308
  });
241
309
  }
242
310
 
@@ -80,6 +80,8 @@ export interface IBuildCommandOptions {
80
80
  cached?: boolean; // Skip builds when Dockerfile content hasn't changed
81
81
  verbose?: boolean; // Stream raw docker build output (default: silent)
82
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)
83
85
  }
84
86
 
85
87
  export interface ICacheEntry {
@@ -7,6 +7,7 @@ import * as DockerModule from './tsdocker.docker.js';
7
7
 
8
8
  import { logger, ora } from './tsdocker.logging.js';
9
9
  import { TsDockerManager } from './classes.tsdockermanager.js';
10
+ import { DockerContext } from './classes.dockercontext.js';
10
11
  import type { IBuildCommandOptions } from './interfaces/index.js';
11
12
  import { commitinfo } from './00_commitinfo_data.js';
12
13
 
@@ -55,6 +56,12 @@ export let run = () => {
55
56
  if (argvArg.verbose) {
56
57
  buildOptions.verbose = true;
57
58
  }
59
+ if (argvArg.parallel) {
60
+ buildOptions.parallel = true;
61
+ if (typeof argvArg.parallel === 'number') {
62
+ buildOptions.parallelConcurrency = argvArg.parallel;
63
+ }
64
+ }
58
65
 
59
66
  await manager.build(buildOptions);
60
67
  logger.log('success', 'Build completed successfully');
@@ -95,6 +102,12 @@ export let run = () => {
95
102
  if (argvArg.verbose) {
96
103
  buildOptions.verbose = true;
97
104
  }
105
+ if (argvArg.parallel) {
106
+ buildOptions.parallel = true;
107
+ if (typeof argvArg.parallel === 'number') {
108
+ buildOptions.parallelConcurrency = argvArg.parallel;
109
+ }
110
+ }
98
111
 
99
112
  // Build images first (if not already built)
100
113
  await manager.build(buildOptions);
@@ -157,6 +170,12 @@ export let run = () => {
157
170
  if (argvArg.verbose) {
158
171
  buildOptions.verbose = true;
159
172
  }
173
+ if (argvArg.parallel) {
174
+ buildOptions.parallel = true;
175
+ if (typeof argvArg.parallel === 'number') {
176
+ buildOptions.parallelConcurrency = argvArg.parallel;
177
+ }
178
+ }
160
179
  await manager.build(buildOptions);
161
180
 
162
181
  // Run tests
@@ -218,27 +237,200 @@ export let run = () => {
218
237
  });
219
238
 
220
239
  tsdockerCli.addCommand('clean').subscribe(async argvArg => {
221
- ora.text('cleaning up docker env...');
222
- if (argvArg.all) {
223
- const smartshellInstance = new plugins.smartshell.Smartshell({
224
- executor: 'bash'
225
- });
226
- ora.text('killing any running docker containers...');
227
- await smartshellInstance.exec(`docker kill $(docker ps -q)`);
240
+ try {
241
+ const autoYes = !!argvArg.y;
242
+ const includeAll = !!argvArg.all;
243
+
244
+ const smartshellInstance = new plugins.smartshell.Smartshell({ executor: 'bash' });
245
+ const interact = new plugins.smartinteract.SmartInteract();
246
+
247
+ // --- Docker context detection ---
248
+ ora.text('detecting docker context...');
249
+ const dockerContext = new DockerContext();
250
+ if (argvArg.context) {
251
+ dockerContext.setContext(argvArg.context as string);
252
+ }
253
+ await dockerContext.detect();
254
+ ora.stop();
255
+ dockerContext.logContextInfo();
256
+
257
+ // --- Helper: parse docker output into resource list ---
258
+ interface IDockerResource {
259
+ id: string;
260
+ display: string;
261
+ }
228
262
 
229
- ora.text('removing stopped containers...');
230
- await smartshellInstance.exec(`docker rm $(docker ps -a -q)`);
263
+ const listResources = async (command: string): Promise<IDockerResource[]> => {
264
+ const result = await smartshellInstance.execSilent(command);
265
+ if (result.exitCode !== 0 || !result.stdout.trim()) {
266
+ return [];
267
+ }
268
+ return result.stdout.trim().split('\n').filter(Boolean).map((line) => {
269
+ const parts = line.split('\t');
270
+ return {
271
+ id: parts[0],
272
+ display: parts.join(' | '),
273
+ };
274
+ });
275
+ };
231
276
 
232
- ora.text('removing images...');
233
- await smartshellInstance.exec(`docker rmi -f $(docker images -q -f dangling=true)`);
277
+ // --- Helper: checkbox selection ---
278
+ const selectResources = async (
279
+ name: string,
280
+ message: string,
281
+ resources: IDockerResource[],
282
+ ): Promise<string[]> => {
283
+ if (autoYes) {
284
+ return resources.map((r) => r.id);
285
+ }
286
+ const answer = await interact.askQuestion({
287
+ name,
288
+ type: 'checkbox',
289
+ message,
290
+ default: [],
291
+ choices: resources.map((r) => ({ name: r.display, value: r.id })),
292
+ });
293
+ return answer.value as string[];
294
+ };
234
295
 
235
- ora.text('removing all other images...');
236
- await smartshellInstance.exec(`docker rmi $(docker images -a -q)`);
296
+ // --- Helper: confirm action ---
297
+ const confirmAction = async (
298
+ name: string,
299
+ message: string,
300
+ ): Promise<boolean> => {
301
+ if (autoYes) {
302
+ return true;
303
+ }
304
+ const answer = await interact.askQuestion({
305
+ name,
306
+ type: 'confirm',
307
+ message,
308
+ default: false,
309
+ });
310
+ return answer.value as boolean;
311
+ };
237
312
 
238
- ora.text('removing all volumes...');
239
- await smartshellInstance.exec(`docker volume rm $(docker volume ls -f dangling=true -q)`);
313
+ // === RUNNING CONTAINERS ===
314
+ const runningContainers = await listResources(
315
+ `docker ps --format '{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}'`
316
+ );
317
+ if (runningContainers.length > 0) {
318
+ logger.log('info', `Found ${runningContainers.length} running container(s)`);
319
+ const selectedIds = await selectResources(
320
+ 'runningContainers',
321
+ 'Select running containers to kill:',
322
+ runningContainers,
323
+ );
324
+ if (selectedIds.length > 0) {
325
+ logger.log('info', `Killing ${selectedIds.length} container(s)...`);
326
+ await smartshellInstance.exec(`docker kill ${selectedIds.join(' ')}`);
327
+ }
328
+ } else {
329
+ logger.log('info', 'No running containers found');
330
+ }
331
+
332
+ // === STOPPED CONTAINERS ===
333
+ const stoppedContainers = await listResources(
334
+ `docker ps -a --filter status=exited --filter status=created --format '{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}'`
335
+ );
336
+ if (stoppedContainers.length > 0) {
337
+ logger.log('info', `Found ${stoppedContainers.length} stopped container(s)`);
338
+ const selectedIds = await selectResources(
339
+ 'stoppedContainers',
340
+ 'Select stopped containers to remove:',
341
+ stoppedContainers,
342
+ );
343
+ if (selectedIds.length > 0) {
344
+ logger.log('info', `Removing ${selectedIds.length} container(s)...`);
345
+ await smartshellInstance.exec(`docker rm ${selectedIds.join(' ')}`);
346
+ }
347
+ } else {
348
+ logger.log('info', 'No stopped containers found');
349
+ }
350
+
351
+ // === DANGLING IMAGES ===
352
+ const danglingImages = await listResources(
353
+ `docker images -f dangling=true --format '{{.ID}}\t{{.Repository}}:{{.Tag}}\t{{.Size}}'`
354
+ );
355
+ if (danglingImages.length > 0) {
356
+ const confirmed = await confirmAction(
357
+ 'removeDanglingImages',
358
+ `Remove ${danglingImages.length} dangling image(s)?`,
359
+ );
360
+ if (confirmed) {
361
+ logger.log('info', `Removing ${danglingImages.length} dangling image(s)...`);
362
+ const ids = danglingImages.map((r) => r.id).join(' ');
363
+ await smartshellInstance.exec(`docker rmi ${ids}`);
364
+ }
365
+ } else {
366
+ logger.log('info', 'No dangling images found');
367
+ }
368
+
369
+ // === ALL IMAGES (only with --all) ===
370
+ if (includeAll) {
371
+ const allImages = await listResources(
372
+ `docker images --format '{{.ID}}\t{{.Repository}}:{{.Tag}}\t{{.Size}}'`
373
+ );
374
+ if (allImages.length > 0) {
375
+ logger.log('info', `Found ${allImages.length} image(s) total`);
376
+ const selectedIds = await selectResources(
377
+ 'allImages',
378
+ 'Select images to remove:',
379
+ allImages,
380
+ );
381
+ if (selectedIds.length > 0) {
382
+ logger.log('info', `Removing ${selectedIds.length} image(s)...`);
383
+ await smartshellInstance.exec(`docker rmi -f ${selectedIds.join(' ')}`);
384
+ }
385
+ } else {
386
+ logger.log('info', 'No images found');
387
+ }
388
+ }
389
+
390
+ // === DANGLING VOLUMES ===
391
+ const danglingVolumes = await listResources(
392
+ `docker volume ls -f dangling=true --format '{{.Name}}\t{{.Driver}}'`
393
+ );
394
+ if (danglingVolumes.length > 0) {
395
+ const confirmed = await confirmAction(
396
+ 'removeDanglingVolumes',
397
+ `Remove ${danglingVolumes.length} dangling volume(s)?`,
398
+ );
399
+ if (confirmed) {
400
+ logger.log('info', `Removing ${danglingVolumes.length} dangling volume(s)...`);
401
+ const names = danglingVolumes.map((r) => r.id).join(' ');
402
+ await smartshellInstance.exec(`docker volume rm ${names}`);
403
+ }
404
+ } else {
405
+ logger.log('info', 'No dangling volumes found');
406
+ }
407
+
408
+ // === ALL VOLUMES (only with --all) ===
409
+ if (includeAll) {
410
+ const allVolumes = await listResources(
411
+ `docker volume ls --format '{{.Name}}\t{{.Driver}}'`
412
+ );
413
+ if (allVolumes.length > 0) {
414
+ logger.log('info', `Found ${allVolumes.length} volume(s) total`);
415
+ const selectedIds = await selectResources(
416
+ 'allVolumes',
417
+ 'Select volumes to remove:',
418
+ allVolumes,
419
+ );
420
+ if (selectedIds.length > 0) {
421
+ logger.log('info', `Removing ${selectedIds.length} volume(s)...`);
422
+ await smartshellInstance.exec(`docker volume rm ${selectedIds.join(' ')}`);
423
+ }
424
+ } else {
425
+ logger.log('info', 'No volumes found');
426
+ }
427
+ }
428
+
429
+ logger.log('success', 'Docker cleanup completed!');
430
+ } catch (err) {
431
+ logger.log('error', `Clean failed: ${(err as Error).message}`);
432
+ process.exit(1);
240
433
  }
241
- ora.finishSuccess('docker environment now is clean!');
242
434
  });
243
435
 
244
436
  tsdockerCli.addCommand('vscode').subscribe(async argvArg => {
@@ -11,6 +11,7 @@ import * as smartlog from '@push.rocks/smartlog';
11
11
  import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-local';
12
12
  import * as smartlogSouceOra from '@push.rocks/smartlog-source-ora';
13
13
  import * as smartopen from '@push.rocks/smartopen';
14
+ import * as smartinteract from '@push.rocks/smartinteract';
14
15
  import * as smartshell from '@push.rocks/smartshell';
15
16
  import * as smartstring from '@push.rocks/smartstring';
16
17
 
@@ -25,6 +26,7 @@ export {
25
26
  smartpromise,
26
27
  qenv,
27
28
  smartcli,
29
+ smartinteract,
28
30
  smartlog,
29
31
  smartlogDestinationLocal,
30
32
  smartlogSouceOra,