@git.zone/cli 3.0.1 → 3.1.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.
@@ -3,6 +3,7 @@ import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-loca
3
3
  import * as smartconfig from '@push.rocks/smartconfig';
4
4
  import * as path from 'path';
5
5
  import * as fs from 'node:fs/promises';
6
+ import * as crypto from 'node:crypto';
6
7
  import * as projectinfo from '@push.rocks/projectinfo';
7
8
  import * as smartcli from '@push.rocks/smartcli';
8
9
  import * as smartpath from '@push.rocks/smartpath';
@@ -15,4 +16,4 @@ import { SmartFs } from '@push.rocks/smartfs';
15
16
  import * as smartinteract from '@push.rocks/smartinteract';
16
17
  import * as smartdelay from '@push.rocks/smartdelay';
17
18
  export declare const smartfs: SmartFs;
18
- export { smartlog, smartlogDestinationLocal, smartconfig, path, fs, projectinfo, smartcli, smartpath, smartpromise, smartupdate, smartshell, smartnetwork, smartfile, smartinteract, smartdelay, };
19
+ export { smartlog, smartlogDestinationLocal, smartconfig, path, fs, crypto, projectinfo, smartcli, smartpath, smartpromise, smartupdate, smartshell, smartnetwork, smartfile, smartinteract, smartdelay, };
@@ -3,6 +3,7 @@ import * as smartlogDestinationLocal from '@push.rocks/smartlog-destination-loca
3
3
  import * as smartconfig from '@push.rocks/smartconfig';
4
4
  import * as path from 'path';
5
5
  import * as fs from 'node:fs/promises';
6
+ import * as crypto from 'node:crypto';
6
7
  import * as projectinfo from '@push.rocks/projectinfo';
7
8
  import * as smartcli from '@push.rocks/smartcli';
8
9
  import * as smartpath from '@push.rocks/smartpath';
@@ -16,5 +17,5 @@ import * as smartinteract from '@push.rocks/smartinteract';
16
17
  import * as smartdelay from '@push.rocks/smartdelay';
17
18
  // Create smartfs instance for filesystem operations
18
19
  export const smartfs = new SmartFs(new SmartFsProviderNode());
19
- export { smartlog, smartlogDestinationLocal, smartconfig, path, fs, projectinfo, smartcli, smartpath, smartpromise, smartupdate, smartshell, smartnetwork, smartfile, smartinteract, smartdelay, };
20
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssd0JBQXdCLE1BQU0sd0NBQXdDLENBQUM7QUFDbkYsT0FBTyxLQUFLLFdBQVcsTUFBTSx5QkFBeUIsQ0FBQztBQUN2RCxPQUFPLEtBQUssSUFBSSxNQUFNLE1BQU0sQ0FBQztBQUM3QixPQUFPLEtBQUssRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ3ZDLE9BQU8sS0FBSyxXQUFXLE1BQU0seUJBQXlCLENBQUM7QUFDdkQsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssU0FBUyxNQUFNLHVCQUF1QixDQUFDO0FBQ25ELE9BQU8sS0FBSyxZQUFZLE1BQU0sMEJBQTBCLENBQUM7QUFDekQsT0FBTyxLQUFLLFdBQVcsTUFBTSx5QkFBeUIsQ0FBQztBQUN2RCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBQ3JELE9BQU8sS0FBSyxZQUFZLE1BQU0sMEJBQTBCLENBQUM7QUFDekQsT0FBTyxLQUFLLFNBQVMsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRCxPQUFPLEVBQUUsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDbkUsT0FBTyxLQUFLLGFBQWEsTUFBTSwyQkFBMkIsQ0FBQztBQUMzRCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBRXJELG9EQUFvRDtBQUNwRCxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsSUFBSSxPQUFPLENBQUMsSUFBSSxtQkFBbUIsRUFBRSxDQUFDLENBQUM7QUFFOUQsT0FBTyxFQUNMLFFBQVEsRUFDUix3QkFBd0IsRUFDeEIsV0FBVyxFQUNYLElBQUksRUFDSixFQUFFLEVBQ0YsV0FBVyxFQUNYLFFBQVEsRUFDUixTQUFTLEVBQ1QsWUFBWSxFQUNaLFdBQVcsRUFDWCxVQUFVLEVBQ1YsWUFBWSxFQUNaLFNBQVMsRUFDVCxhQUFhLEVBQ2IsVUFBVSxHQUNYLENBQUMifQ==
20
+ export { smartlog, smartlogDestinationLocal, smartconfig, path, fs, crypto, projectinfo, smartcli, smartpath, smartpromise, smartupdate, smartshell, smartnetwork, smartfile, smartinteract, smartdelay, };
21
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicGx1Z2lucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL3BsdWdpbnMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssd0JBQXdCLE1BQU0sd0NBQXdDLENBQUM7QUFDbkYsT0FBTyxLQUFLLFdBQVcsTUFBTSx5QkFBeUIsQ0FBQztBQUN2RCxPQUFPLEtBQUssSUFBSSxNQUFNLE1BQU0sQ0FBQztBQUM3QixPQUFPLEtBQUssRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQ3ZDLE9BQU8sS0FBSyxNQUFNLE1BQU0sYUFBYSxDQUFDO0FBQ3RDLE9BQU8sS0FBSyxXQUFXLE1BQU0seUJBQXlCLENBQUM7QUFDdkQsT0FBTyxLQUFLLFFBQVEsTUFBTSxzQkFBc0IsQ0FBQztBQUNqRCxPQUFPLEtBQUssU0FBUyxNQUFNLHVCQUF1QixDQUFDO0FBQ25ELE9BQU8sS0FBSyxZQUFZLE1BQU0sMEJBQTBCLENBQUM7QUFDekQsT0FBTyxLQUFLLFdBQVcsTUFBTSx5QkFBeUIsQ0FBQztBQUN2RCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBQ3JELE9BQU8sS0FBSyxZQUFZLE1BQU0sMEJBQTBCLENBQUM7QUFDekQsT0FBTyxLQUFLLFNBQVMsTUFBTSx1QkFBdUIsQ0FBQztBQUNuRCxPQUFPLEVBQUUsT0FBTyxFQUFFLG1CQUFtQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDbkUsT0FBTyxLQUFLLGFBQWEsTUFBTSwyQkFBMkIsQ0FBQztBQUMzRCxPQUFPLEtBQUssVUFBVSxNQUFNLHdCQUF3QixDQUFDO0FBRXJELG9EQUFvRDtBQUNwRCxNQUFNLENBQUMsTUFBTSxPQUFPLEdBQUcsSUFBSSxPQUFPLENBQUMsSUFBSSxtQkFBbUIsRUFBRSxDQUFDLENBQUM7QUFFOUQsT0FBTyxFQUNMLFFBQVEsRUFDUix3QkFBd0IsRUFDeEIsV0FBVyxFQUNYLElBQUksRUFDSixFQUFFLEVBQ0YsTUFBTSxFQUNOLFdBQVcsRUFDWCxRQUFRLEVBQ1IsU0FBUyxFQUNULFlBQVksRUFDWixXQUFXLEVBQ1gsVUFBVSxFQUNWLFlBQVksRUFDWixTQUFTLEVBQ1QsYUFBYSxFQUNiLFVBQVUsR0FDWCxDQUFDIn0=
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@git.zone/cli",
3
3
  "private": false,
4
- "version": "3.0.1",
4
+ "version": "3.1.0",
5
5
  "description": "A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.",
6
6
  "main": "dist_ts/index.js",
7
7
  "typings": "dist_ts/index.d.ts",
@@ -40,9 +40,9 @@
40
40
  "homepage": "https://code.foss.global/git.zone/cli#README",
41
41
  "devDependencies": {
42
42
  "@git.zone/tsbuild": "^4.4.2",
43
- "@git.zone/tsrun": "^2.0.5",
44
- "@git.zone/tstest": "^3.6.6",
45
- "@types/node": "^25.9.3"
43
+ "@git.zone/tsrun": "^2.0.6",
44
+ "@git.zone/tstest": "^3.6.7",
45
+ "@types/node": "^26.1.1"
46
46
  },
47
47
  "dependencies": {
48
48
  "@git.zone/tsdoc": "^2.1.1",
@@ -69,7 +69,7 @@
69
69
  "@push.rocks/smartpath": "^6.0.0",
70
70
  "@push.rocks/smartpromise": "^4.2.3",
71
71
  "@push.rocks/smartscaf": "^4.0.22",
72
- "@push.rocks/smartshell": "^3.5.0",
72
+ "@push.rocks/smartshell": "^4.0.0",
73
73
  "@push.rocks/smartunique": "^3.0.9",
74
74
  "@push.rocks/smartupdate": "^2.0.7",
75
75
  "prettier": "^3.8.1"
package/readme.md CHANGED
@@ -243,6 +243,8 @@ CLI workflow config lives under `@git.zone/cli` in `.smartconfig.json`. Docker b
243
243
  "docker": {
244
244
  "enabled": true,
245
245
  "engine": "tsdocker",
246
+ "registry": "registry.gitlab.com",
247
+ "buildRegistries": ["registry.gitlab.com"],
246
248
  "patterns": [],
247
249
  "cached": true,
248
250
  "parallel": true
@@ -266,12 +268,28 @@ NPM registries belong only here:
266
268
  @git.zone/cli.release.targets.npm.registries
267
269
  ```
268
270
 
269
- Docker registries belong only here and should be registry hosts without `http://` or `https://`:
271
+ Canonical Docker destinations and repository mappings belong here. Registry
272
+ values should be hosts without `http://` or `https://`:
270
273
 
271
274
  ```text
272
275
  @git.zone/tsdocker.registries
273
276
  ```
274
277
 
278
+ The Docker release target runs the project's pinned `@git.zone/tsdocker` through
279
+ `pnpm exec`; install it in each Docker-producing project:
280
+
281
+ ```bash
282
+ pnpm add --save-dev @git.zone/tsdocker
283
+ ```
284
+
285
+ Set `release.targets.docker.registry` to select one destination already declared
286
+ by the tSDocker config when a release must not publish to every configured
287
+ registry.
288
+ Set `release.targets.docker.buildRegistries` when the build should authenticate
289
+ only the configured registries used by private base images; an empty list keeps
290
+ the compatibility default of authenticating every configured registry. Bounded
291
+ build authentication requires project-local `@git.zone/tsdocker` 3.3.0 or newer.
292
+
275
293
  Useful config commands:
276
294
 
277
295
  ```bash
@@ -458,6 +476,20 @@ gitzone services logs mongo 50
458
476
 
459
477
  Service config is stored in `.nogit/env.json`. Data is stored below `.nogit/`, so it stays out of Git.
460
478
 
479
+ GitZone derives new MinIO bucket names from the project name using S3 naming
480
+ rules. When loading older configuration it repairs only the exact generated
481
+ `<project>-documents` value if that value is invalid; custom bucket names are
482
+ never rewritten. `services start minio` validates custom names, recreates a
483
+ container whose configured credentials or ports drifted, authenticates the
484
+ configured alias, creates the bucket idempotently, and verifies it before
485
+ reporting success. Before GitZone starts, recreates, or executes inside an
486
+ existing MinIO container, current-project ownership must be proven by exact
487
+ GitZone labels or one unambiguous legacy registry claim. Every lifecycle
488
+ operation after discovery uses the container's immutable id. If startup fails,
489
+ GitZone stops only containers that this invocation started; it never stops a
490
+ container that was already running. Invalid custom names do not block read-only
491
+ or recovery commands such as `status`, `logs`, `stop`, `remove`, and `clean`.
492
+
461
493
  ### Consuming a service programmatically
462
494
 
463
495
  `gitzone services status --json` emits only JSON on stdout, so a test suite or
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@git.zone/cli',
6
- version: '3.0.1',
6
+ version: '3.1.0',
7
7
  description: 'A comprehensive CLI tool for enhancing and managing local development workflows with gitzone utilities, focusing on project setup, version control, code formatting, and template management.'
8
8
  }
@@ -0,0 +1,30 @@
1
+ import * as plugins from './plugins.js';
2
+
3
+ export type TTsdockerBinaryAccess = (binaryPathArg: string) => Promise<void>;
4
+
5
+ export const getProjectTsdockerBinPath = (
6
+ projectRootArg: string,
7
+ platformArg: NodeJS.Platform = process.platform,
8
+ ): string => {
9
+ return plugins.path.join(
10
+ projectRootArg,
11
+ 'node_modules',
12
+ '.bin',
13
+ platformArg === 'win32' ? 'tsdocker.CMD' : 'tsdocker',
14
+ );
15
+ };
16
+
17
+ export const hasInstalledProjectTsdockerBinary = async (
18
+ projectRootArg: string,
19
+ platformArg: NodeJS.Platform = process.platform,
20
+ accessArg: TTsdockerBinaryAccess = async (binaryPathArg) => {
21
+ await plugins.fs.access(binaryPathArg);
22
+ },
23
+ ): Promise<boolean> => {
24
+ try {
25
+ await accessArg(getProjectTsdockerBinPath(projectRootArg, platformArg));
26
+ return true;
27
+ } catch {
28
+ return false;
29
+ }
30
+ };
@@ -53,6 +53,8 @@ export interface IReleaseNpmTargetConfig {
53
53
  export interface IReleaseDockerTargetConfig {
54
54
  enabled?: boolean;
55
55
  engine?: "tsdocker";
56
+ registry?: string;
57
+ buildRegistries?: string[];
56
58
  patterns?: string[];
57
59
  cached?: boolean;
58
60
  parallel?: boolean | number;
@@ -116,6 +118,8 @@ export interface IResolvedReleaseWorkflow {
116
118
  npmAlreadyPublished: "success" | "error";
117
119
  dockerEnabled: boolean;
118
120
  dockerEngine: "tsdocker";
121
+ dockerRegistry?: string;
122
+ dockerBuildRegistries: string[];
119
123
  dockerPatterns: string[];
120
124
  dockerCached: boolean;
121
125
  dockerParallel: boolean | number;
@@ -145,6 +149,39 @@ const unique = <T>(items: T[]): T[] => {
145
149
  return result;
146
150
  };
147
151
 
152
+ export const supportsBoundedBuildRegistryAuthentication = (
153
+ versionOutputArg: string,
154
+ ): boolean => {
155
+ const match = versionOutputArg.match(/(\d+)\.(\d+)\.(\d+)/);
156
+ if (!match) {
157
+ return false;
158
+ }
159
+ const major = Number(match[1]);
160
+ const minor = Number(match[2]);
161
+ return major > 3 || (major === 3 && minor >= 3);
162
+ };
163
+
164
+ const isStringRecord = (valueArg: unknown): valueArg is Record<string, unknown> => {
165
+ return typeof valueArg === "object" && valueArg !== null && !Array.isArray(valueArg);
166
+ };
167
+
168
+ export const getDeclaredTsdockerRange = (packageJsonArg: unknown): string | undefined => {
169
+ if (!isStringRecord(packageJsonArg)) {
170
+ return undefined;
171
+ }
172
+ for (const dependencyField of ["devDependencies", "dependencies"] as const) {
173
+ const dependencies = packageJsonArg[dependencyField];
174
+ if (!isStringRecord(dependencies)) {
175
+ continue;
176
+ }
177
+ const versionRange = dependencies["@git.zone/tsdocker"];
178
+ if (typeof versionRange === "string" && versionRange.trim()) {
179
+ return versionRange.trim();
180
+ }
181
+ }
182
+ return undefined;
183
+ };
184
+
148
185
  const normalizeConfirmation = (
149
186
  value: unknown,
150
187
  fallback: TConfirmationMode,
@@ -395,6 +432,15 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
395
432
  npmAlreadyPublished: npmConfig.alreadyPublished || "success",
396
433
  dockerEnabled,
397
434
  dockerEngine: "tsdocker",
435
+ dockerRegistry: typeof dockerConfig.registry === "string" && dockerConfig.registry.trim()
436
+ ? dockerConfig.registry.trim()
437
+ : undefined,
438
+ dockerBuildRegistries: Array.isArray(dockerConfig.buildRegistries)
439
+ ? dockerConfig.buildRegistries
440
+ .filter((registryArg): registryArg is string => typeof registryArg === "string")
441
+ .map((registryArg) => registryArg.trim())
442
+ .filter(Boolean)
443
+ : [],
398
444
  dockerPatterns: Array.isArray(dockerConfig.patterns) ? dockerConfig.patterns : [],
399
445
  dockerCached: dockerConfig.cached ?? false,
400
446
  dockerParallel: dockerConfig.parallel ?? false,
@@ -19,6 +19,11 @@ import {
19
19
  CURRENT_GITZONE_CLI_SCHEMA_VERSION,
20
20
  migrateSmartconfigData,
21
21
  } from "../helpers.smartconfigmigrations.js";
22
+ import {
23
+ getDeclaredTsdockerRange,
24
+ supportsBoundedBuildRegistryAuthentication,
25
+ } from "../helpers.workflow.js";
26
+ import { hasInstalledProjectTsdockerBinary } from "../helpers.tsdocker.js";
22
27
 
23
28
  export { ReleaseConfig, CommitConfig };
24
29
 
@@ -871,6 +876,20 @@ async function handleRelease(mode: ICliMode): Promise<void> {
871
876
  }
872
877
 
873
878
  if (enabledTargets.includes("docker")) {
879
+ const registry = await askValue<string>(interactInstance, {
880
+ type: "input",
881
+ name: "dockerRegistry",
882
+ message: "Single tsdocker registry target (empty means all configured registries):",
883
+ default: currentTargets.docker?.registry || "",
884
+ });
885
+ const buildRegistries = await askValue<string>(interactInstance, {
886
+ type: "input",
887
+ name: "dockerBuildRegistries",
888
+ message: "Registries to authenticate before Docker builds (comma-separated, empty means all):",
889
+ default: Array.isArray(currentTargets.docker?.buildRegistries)
890
+ ? currentTargets.docker.buildRegistries.join(", ")
891
+ : "",
892
+ });
874
893
  const patterns = await askValue<string>(interactInstance, {
875
894
  type: "input",
876
895
  name: "dockerPatterns",
@@ -906,6 +925,8 @@ async function handleRelease(mode: ICliMode): Promise<void> {
906
925
  releaseTargets.docker = {
907
926
  enabled: true,
908
927
  engine: "tsdocker",
928
+ registry: registry.trim() || undefined,
929
+ buildRegistries: parseCsv(buildRegistries),
909
930
  patterns: parseCsv(patterns),
910
931
  cached,
911
932
  parallel: parseDockerParallel(parallel),
@@ -1296,6 +1317,7 @@ function parseConfigValue(rawValue: string): any {
1296
1317
  ) {
1297
1318
  return JSON.parse(trimmedValue);
1298
1319
  }
1320
+
1299
1321
  return rawValue;
1300
1322
  }
1301
1323
 
@@ -1963,6 +1985,36 @@ async function validateDockerTarget(
1963
1985
  });
1964
1986
  }
1965
1987
 
1988
+ if (
1989
+ dockerTarget.registry !== undefined
1990
+ && (typeof dockerTarget.registry !== "string"
1991
+ || !dockerTarget.registry.trim()
1992
+ || /^https?:\/\//.test(dockerTarget.registry))
1993
+ ) {
1994
+ findings.push({
1995
+ level: "error",
1996
+ message: `Invalid tsdocker release registry: ${formatValue(dockerTarget.registry)}`,
1997
+ fix: "Use one configured registry host without a protocol, or remove release.targets.docker.registry.",
1998
+ });
1999
+ }
2000
+
2001
+ if (
2002
+ dockerTarget.buildRegistries !== undefined
2003
+ && (!Array.isArray(dockerTarget.buildRegistries)
2004
+ || dockerTarget.buildRegistries.some((registryArg: unknown) =>
2005
+ typeof registryArg !== "string"
2006
+ || !registryArg.trim()
2007
+ || /^https?:\/\//.test(registryArg)
2008
+ )
2009
+ || new Set(dockerTarget.buildRegistries).size !== dockerTarget.buildRegistries.length)
2010
+ ) {
2011
+ findings.push({
2012
+ level: "error",
2013
+ message: "Invalid tsdocker build registry authentication list",
2014
+ fix: "Use unique configured registry hosts without protocols, or remove release.targets.docker.buildRegistries.",
2015
+ });
2016
+ }
2017
+
1966
2018
  if (!isValidDockerParallel(dockerTarget.parallel)) {
1967
2019
  findings.push({
1968
2020
  level: "error",
@@ -1982,7 +2034,7 @@ async function validateDockerTarget(
1982
2034
  validateTsdockerProjectConfig(tsdockerConfig, findings);
1983
2035
  }
1984
2036
 
1985
- await validateTsdockerCommand(findings);
2037
+ await validateTsdockerCommand(findings, dockerTarget.buildRegistries);
1986
2038
 
1987
2039
  findings.push({
1988
2040
  level: "ok",
@@ -2064,30 +2116,66 @@ function validateTsdockerProjectConfig(
2064
2116
  });
2065
2117
  }
2066
2118
 
2067
- async function validateTsdockerCommand(findings: IDoctorFinding[]): Promise<void> {
2119
+ async function validateTsdockerCommand(
2120
+ findings: IDoctorFinding[],
2121
+ buildRegistriesArg: unknown,
2122
+ ): Promise<void> {
2123
+ let packageJson: unknown;
2124
+ try {
2125
+ packageJson = JSON.parse(
2126
+ await plugins.fs.readFile(plugins.path.join(process.cwd(), "package.json"), "utf8"),
2127
+ );
2128
+ } catch {
2129
+ packageJson = undefined;
2130
+ }
2131
+ if (!getDeclaredTsdockerRange(packageJson)) {
2132
+ findings.push({
2133
+ level: "error",
2134
+ message: "Docker release target requires project-local @git.zone/tsdocker",
2135
+ fix: "Run pnpm add --save-dev @git.zone/tsdocker.",
2136
+ });
2137
+ }
2138
+ if (!(await hasInstalledProjectTsdockerBinary(process.cwd()))) {
2139
+ findings.push({
2140
+ level: "error",
2141
+ message: "Project-local tSDocker binary is not installed",
2142
+ fix: "Run pnpm install after declaring @git.zone/tsdocker.",
2143
+ });
2144
+ }
2068
2145
  const smartshellInstance = new plugins.smartshell.Smartshell({
2069
2146
  executor: "bash",
2070
2147
  sourceFilePaths: [],
2071
2148
  });
2072
2149
  try {
2073
2150
  const result = await smartshellInstance.execSpawn(
2074
- "tsdocker",
2075
- ["--version"],
2151
+ "pnpm",
2152
+ ["exec", "tsdocker", "--version"],
2076
2153
  { silent: true, timeout: 8000 },
2077
2154
  );
2078
2155
  if (result.exitCode === 0) {
2079
2156
  findings.push({ level: "ok", message: "tsdocker command is available" });
2157
+ if (
2158
+ Array.isArray(buildRegistriesArg)
2159
+ && buildRegistriesArg.length > 0
2160
+ && !supportsBoundedBuildRegistryAuthentication(result.combinedOutput)
2161
+ ) {
2162
+ findings.push({
2163
+ level: "error",
2164
+ message: "Bounded Docker build authentication requires @git.zone/tsdocker >= 3.3.0",
2165
+ fix: "Upgrade the project's @git.zone/tsdocker dev dependency to at least 3.3.0.",
2166
+ });
2167
+ }
2080
2168
  } else {
2081
2169
  findings.push({
2082
2170
  level: "error",
2083
- message: "tsdocker command is not available",
2084
- fix: "Install @git.zone/tsdocker globally or make it available on PATH.",
2171
+ message: "Project-local tsdocker command is not available",
2172
+ fix: "Install @git.zone/tsdocker as a project dev dependency.",
2085
2173
  });
2086
2174
  }
2087
2175
  } catch (error) {
2088
2176
  findings.push({
2089
2177
  level: "error",
2090
- message: "Could not execute tsdocker",
2178
+ message: "Could not execute project-local tsdocker",
2091
2179
  fix: error instanceof Error ? error.message : String(error),
2092
2180
  });
2093
2181
  }
@@ -71,15 +71,14 @@ const dockerCommand = `timeout ${dockerTimeoutSeconds}s docker`;
71
71
  export class DockerPruner {
72
72
  private smartshell = new plugins.smartshell.Smartshell({ executor: 'bash' });
73
73
 
74
- private async run(commandArg: string): Promise<{ exitCode: number; stdout: string }> {
75
- const result = await this.smartshell.execSilent(commandArg);
76
- return { exitCode: result.exitCode, stdout: result.stdout || '' };
74
+ private async run(commandArg: string): Promise<plugins.smartshell.IExecResult> {
75
+ return this.smartshell.execSilent(commandArg);
77
76
  }
78
77
 
79
78
  private async listLines(commandArg: string): Promise<string[]> {
80
79
  const result = await this.run(commandArg);
81
80
  if (result.exitCode !== 0) {
82
- throw new Error(result.stdout || `docker command failed: ${commandArg}`);
81
+ throw new Error(result.combinedOutput);
83
82
  }
84
83
  if (!result.stdout.trim()) {
85
84
  return [];
@@ -99,7 +98,7 @@ export class DockerPruner {
99
98
  `${dockerCommand} inspect ${idsArg.map((id) => shellQuote(id)).join(' ')}`,
100
99
  );
101
100
  if (result.exitCode !== 0) {
102
- throw new Error(result.stdout || 'docker inspect failed');
101
+ throw new Error(result.combinedOutput);
103
102
  }
104
103
  try {
105
104
  const parsed = JSON.parse(result.stdout);
@@ -287,7 +286,7 @@ export class DockerPruner {
287
286
  }
288
287
  const result = await this.run(`${dockerCommand} rm ${shellQuote(planned.id)}`);
289
288
  if (result.exitCode !== 0) {
290
- throw new Error(`Failed to remove container ${planned.name}: ${result.stdout}`);
289
+ throw new Error(`Failed to remove container ${planned.name}: ${result.combinedOutput}`);
291
290
  }
292
291
  removedContainers.push(planned.name);
293
292
  logger.log('ok', `Removed container ${planned.name}`);
@@ -322,7 +321,7 @@ export class DockerPruner {
322
321
  }
323
322
  const result = await this.run(`${dockerCommand} volume rm ${shellQuote(planned.name)}`);
324
323
  if (result.exitCode !== 0) {
325
- throw new Error(`Failed to remove volume ${planned.name}: ${result.stdout}`);
324
+ throw new Error(`Failed to remove volume ${planned.name}: ${result.combinedOutput}`);
326
325
  }
327
326
  removedVolumes.push(planned.name);
328
327
  logger.log('ok', `Removed volume ${planned.name}`);
@@ -9,10 +9,13 @@ import {
9
9
  readPendingChangelog,
10
10
  } from "../helpers.changelog.js";
11
11
  import {
12
+ getDeclaredTsdockerRange,
12
13
  resolveReleaseWorkflow,
14
+ supportsBoundedBuildRegistryAuthentication,
13
15
  type IResolvedReleaseWorkflow,
14
16
  } from "../helpers.workflow.js";
15
17
  import * as commitHelpers from "../mod_commit/mod.helpers.js";
18
+ import { hasInstalledProjectTsdockerBinary } from "../helpers.tsdocker.js";
16
19
 
17
20
  type TTargetStatus = "success" | "already-published" | "skipped" | "failed";
18
21
 
@@ -247,7 +250,7 @@ async function runNpmTarget(
247
250
  for (const registry of workflow.npmRegistries) {
248
251
  const command = `pnpm publish --registry=${registry} --access=${workflow.npmAccessLevel}`;
249
252
  const result = await smartshellInstance.exec(command);
250
- const output = `${result.stdout || ""}\n${(result as any).stderr || ""}\n${(result as any).combinedOutput || ""}`;
253
+ const output = result.combinedOutput;
251
254
  if (result.exitCode === 0) {
252
255
  results.push({ target: registry, status: "success" });
253
256
  } else if (isAlreadyPublishedOutput(output) && workflow.npmAlreadyPublished === "success") {
@@ -267,9 +270,46 @@ async function runDockerTarget(
267
270
  return [{ target: "docker", status: "skipped", message: "disabled" }];
268
271
  }
269
272
 
273
+ let packageJson: unknown;
274
+ try {
275
+ packageJson = JSON.parse(
276
+ await plugins.fs.readFile(plugins.path.join(paths.cwd, "package.json"), "utf8"),
277
+ );
278
+ } catch {
279
+ packageJson = undefined;
280
+ }
281
+ if (!getDeclaredTsdockerRange(packageJson)) {
282
+ return [{
283
+ target: "tsdocker",
284
+ status: "failed",
285
+ message: "Docker releases require @git.zone/tsdocker as a project dependency",
286
+ }];
287
+ }
288
+ if (!(await hasInstalledProjectTsdockerBinary(paths.cwd))) {
289
+ return [{
290
+ target: "tsdocker",
291
+ status: "failed",
292
+ message: "Docker releases require the installed project-local tSDocker binary",
293
+ }];
294
+ }
295
+
296
+ if (workflow.dockerBuildRegistries.length > 0) {
297
+ const versionResult = await smartshellInstance.exec("pnpm exec tsdocker --version");
298
+ if (
299
+ versionResult.exitCode !== 0
300
+ || !supportsBoundedBuildRegistryAuthentication(versionResult.combinedOutput)
301
+ ) {
302
+ return [{
303
+ target: "tsdocker",
304
+ status: "failed",
305
+ message: "release.targets.docker.buildRegistries requires project-local @git.zone/tsdocker >= 3.3.0",
306
+ }];
307
+ }
308
+ }
309
+
270
310
  const command = buildTsdockerPushCommand(workflow);
271
311
  const result = await smartshellInstance.exec(command);
272
- const output = `${result.stdout || ""}\n${(result as any).stderr || ""}\n${(result as any).combinedOutput || ""}`;
312
+ const output = result.combinedOutput;
273
313
  return [{
274
314
  target: workflow.dockerPatterns.length > 0
275
315
  ? `tsdocker:${workflow.dockerPatterns.join(",")}`
@@ -279,8 +319,25 @@ async function runDockerTarget(
279
319
  }];
280
320
  }
281
321
 
282
- function buildTsdockerPushCommand(workflow: IResolvedReleaseWorkflow): string {
283
- const commandParts = ["tsdocker", "push"];
322
+ type TDockerCommandWorkflow = Pick<
323
+ IResolvedReleaseWorkflow,
324
+ | "dockerRegistry"
325
+ | "dockerBuildRegistries"
326
+ | "dockerNoBuild"
327
+ | "dockerCached"
328
+ | "dockerParallel"
329
+ | "dockerContext"
330
+ | "dockerPatterns"
331
+ >;
332
+
333
+ export function buildTsdockerPushCommand(workflow: TDockerCommandWorkflow): string {
334
+ const commandParts = ["pnpm", "exec", "tsdocker", "push"];
335
+ if (workflow.dockerRegistry) {
336
+ commandParts.push(`--registry=${shellQuote(workflow.dockerRegistry)}`);
337
+ }
338
+ if (workflow.dockerBuildRegistries.length > 0) {
339
+ commandParts.push(`--build-registries=${shellQuote(workflow.dockerBuildRegistries.join(","))}`);
340
+ }
284
341
  if (workflow.dockerNoBuild) {
285
342
  commandParts.push("--no-build");
286
343
  }
@@ -328,6 +385,8 @@ function printReleasePlan(workflow: IResolvedReleaseWorkflow): void {
328
385
  }
329
386
  if (workflow.targets.includes("docker")) {
330
387
  console.log(`docker engine: ${workflow.dockerEngine}`);
388
+ console.log(`docker registry: ${workflow.dockerRegistry || "all configured registries"}`);
389
+ console.log(`docker build authentication: ${workflow.dockerBuildRegistries.length > 0 ? workflow.dockerBuildRegistries.join(", ") : "all configured registries"}`);
331
390
  console.log(`docker patterns: ${workflow.dockerPatterns.length > 0 ? workflow.dockerPatterns.join(", ") : "all Dockerfiles"}`);
332
391
  console.log(`docker options: ${formatDockerOptions(workflow)}`);
333
392
  }
@@ -336,6 +395,10 @@ function printReleasePlan(workflow: IResolvedReleaseWorkflow): void {
336
395
 
337
396
  function formatDockerOptions(workflow: IResolvedReleaseWorkflow): string {
338
397
  const options: string[] = [];
398
+ if (workflow.dockerRegistry) options.push(`registry=${workflow.dockerRegistry}`);
399
+ if (workflow.dockerBuildRegistries.length > 0) {
400
+ options.push(`buildRegistries=${workflow.dockerBuildRegistries.join(",")}`);
401
+ }
339
402
  if (workflow.dockerCached) options.push("cached");
340
403
  if (workflow.dockerParallel) options.push(`parallel=${workflow.dockerParallel === true ? "true" : workflow.dockerParallel}`);
341
404
  if (workflow.dockerNoBuild) options.push("no-build");