@git.zone/cli 2.18.1 → 2.19.1
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.
- package/.smartconfig.json +2 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/gitzone.cli.d.ts +1 -1
- package/dist_ts/gitzone.cli.js +100 -126
- package/dist_ts/helpers.climode.d.ts +2 -0
- package/dist_ts/helpers.climode.js +31 -2
- package/dist_ts/helpers.smartconfigmigrations.js +53 -4
- package/dist_ts/helpers.workflow.d.ts +12 -2
- package/dist_ts/helpers.workflow.js +9 -2
- package/dist_ts/mod_config/index.js +254 -29
- package/dist_ts/mod_format/classes.baseformatter.d.ts +3 -1
- package/dist_ts/mod_format/classes.baseformatter.js +7 -1
- package/dist_ts/mod_format/classes.formatplanner.d.ts +2 -0
- package/dist_ts/mod_format/classes.formatplanner.js +48 -4
- package/dist_ts/mod_format/formatters/license.formatter.d.ts +4 -1
- package/dist_ts/mod_format/formatters/license.formatter.js +32 -10
- package/dist_ts/mod_format/formatters/prettier.formatter.d.ts +0 -3
- package/dist_ts/mod_format/formatters/prettier.formatter.js +53 -62
- package/dist_ts/mod_format/index.d.ts +1 -1
- package/dist_ts/mod_format/index.js +237 -8
- package/dist_ts/mod_format/interfaces.format.d.ts +7 -11
- package/dist_ts/mod_format/interfaces.format.js +1 -1
- package/dist_ts/mod_release/index.js +50 -23
- package/dist_ts/mod_standard/index.js +2 -1
- package/package.json +1 -1
- package/readme.hints.md +10 -0
- package/readme.md +31 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/gitzone.cli.ts +104 -144
- package/ts/helpers.climode.ts +36 -1
- package/ts/helpers.smartconfigmigrations.ts +57 -3
- package/ts/helpers.workflow.ts +20 -3
- package/ts/mod_config/index.ts +278 -29
- package/ts/mod_format/classes.baseformatter.ts +13 -1
- package/ts/mod_format/classes.formatplanner.ts +66 -4
- package/ts/mod_format/formatters/license.formatter.ts +43 -15
- package/ts/mod_format/formatters/prettier.formatter.ts +54 -66
- package/ts/mod_format/index.ts +289 -8
- package/ts/mod_format/interfaces.format.ts +8 -11
- package/ts/mod_release/index.ts +46 -23
- package/ts/mod_standard/index.ts +1 -0
package/ts/helpers.climode.ts
CHANGED
|
@@ -88,6 +88,41 @@ const parseRawArgv = (argv: string[]): TArgSource => {
|
|
|
88
88
|
return parsedArgv;
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
+
export const parseCliArgv = parseRawArgv;
|
|
92
|
+
|
|
93
|
+
export const getProcessUserArgv = (): string[] => {
|
|
94
|
+
const rawArgv = process.argv;
|
|
95
|
+
const argv0Base = (rawArgv[0] || "").split(/[\\/]/).pop()?.toLowerCase();
|
|
96
|
+
const runtimeNames = new Set([
|
|
97
|
+
"node",
|
|
98
|
+
"node.exe",
|
|
99
|
+
"nodejs",
|
|
100
|
+
"nodejs.exe",
|
|
101
|
+
"bun",
|
|
102
|
+
"bun.exe",
|
|
103
|
+
"deno",
|
|
104
|
+
"deno.exe",
|
|
105
|
+
"tsx",
|
|
106
|
+
"tsx.exe",
|
|
107
|
+
"ts-node",
|
|
108
|
+
"ts-node.exe",
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
if (!runtimeNames.has(argv0Base || "")) {
|
|
112
|
+
return rawArgv.slice();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const firstUserArg = rawArgv[1] || "";
|
|
116
|
+
const firstUserArgLooksLikeScript =
|
|
117
|
+
firstUserArg.includes("/") ||
|
|
118
|
+
firstUserArg.endsWith(".js") ||
|
|
119
|
+
firstUserArg.endsWith(".ts") ||
|
|
120
|
+
firstUserArg.endsWith(".mjs") ||
|
|
121
|
+
firstUserArg.endsWith(".cjs");
|
|
122
|
+
|
|
123
|
+
return rawArgv.slice(firstUserArgLooksLikeScript ? 2 : 1);
|
|
124
|
+
};
|
|
125
|
+
|
|
91
126
|
const normalizeOutputMode = (value: unknown): TCliOutputMode | undefined => {
|
|
92
127
|
if (value === "human" || value === "plain" || value === "json") {
|
|
93
128
|
return value;
|
|
@@ -171,7 +206,7 @@ export const getCliMode = async (
|
|
|
171
206
|
|
|
172
207
|
export const getRawCliMode = async (): Promise<ICliMode> => {
|
|
173
208
|
const cliConfig = await getCliModeConfig();
|
|
174
|
-
const rawArgv = parseRawArgv(
|
|
209
|
+
const rawArgv = parseRawArgv(getProcessUserArgv());
|
|
175
210
|
return resolveCliMode(rawArgv, cliConfig);
|
|
176
211
|
};
|
|
177
212
|
|
|
@@ -19,6 +19,38 @@ const ensureObject = (parent: Record<string, any>, key: string): Record<string,
|
|
|
19
19
|
return parent[key];
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
+
const normalizeRegistryList = (registries: unknown[]): string[] => {
|
|
23
|
+
const result: string[] = [];
|
|
24
|
+
for (const registry of registries) {
|
|
25
|
+
if (typeof registry !== "string" || !registry.trim()) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const normalizedRegistry = normalizeRegistryUrl(registry);
|
|
29
|
+
if (!result.includes(normalizedRegistry)) {
|
|
30
|
+
result.push(normalizedRegistry);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const migrateLegacyReleaseArray = (smartconfigJson: Record<string, any>): boolean => {
|
|
37
|
+
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
|
|
38
|
+
if (!Array.isArray(cliConfig.release)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const registries = normalizeRegistryList(cliConfig.release);
|
|
43
|
+
cliConfig.release = {
|
|
44
|
+
targets: {
|
|
45
|
+
npm: {
|
|
46
|
+
enabled: registries.length > 0,
|
|
47
|
+
registries,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
return true;
|
|
52
|
+
};
|
|
53
|
+
|
|
22
54
|
const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean => {
|
|
23
55
|
let migrated = false;
|
|
24
56
|
const migrations = [
|
|
@@ -50,9 +82,9 @@ const migrateNamespaceKeys = (smartconfigJson: Record<string, any>): boolean =>
|
|
|
50
82
|
|
|
51
83
|
const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
|
|
52
84
|
const cliConfig = ensureObject(smartconfigJson, CLI_NAMESPACE);
|
|
85
|
+
let migrated = migrateLegacyReleaseArray(smartconfigJson);
|
|
53
86
|
const releaseConfig = ensureObject(cliConfig, "release");
|
|
54
87
|
|
|
55
|
-
let migrated = false;
|
|
56
88
|
const targets = ensureObject(releaseConfig, "targets");
|
|
57
89
|
const shipzoneConfig = smartconfigJson["@ship.zone/szci"];
|
|
58
90
|
|
|
@@ -68,8 +100,10 @@ const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
|
|
|
68
100
|
migrated = true;
|
|
69
101
|
}
|
|
70
102
|
|
|
71
|
-
if (isPlainObject(releaseConfig.docker)
|
|
72
|
-
targets.docker =
|
|
103
|
+
if (isPlainObject(releaseConfig.docker)) {
|
|
104
|
+
targets.docker = isPlainObject(targets.docker)
|
|
105
|
+
? { ...releaseConfig.docker, ...targets.docker }
|
|
106
|
+
: releaseConfig.docker;
|
|
73
107
|
delete releaseConfig.docker;
|
|
74
108
|
migrated = true;
|
|
75
109
|
}
|
|
@@ -141,11 +175,27 @@ const migrateToV2 = (smartconfigJson: Record<string, any>): boolean => {
|
|
|
141
175
|
if (dockerTarget.enabled === undefined) {
|
|
142
176
|
dockerTarget.enabled = true;
|
|
143
177
|
}
|
|
178
|
+
dockerTarget.engine = "tsdocker";
|
|
144
179
|
}
|
|
145
180
|
delete releaseConfig.steps;
|
|
146
181
|
migrated = true;
|
|
147
182
|
}
|
|
148
183
|
|
|
184
|
+
if (isPlainObject(targets.docker)) {
|
|
185
|
+
if (targets.docker.images) {
|
|
186
|
+
delete targets.docker.images;
|
|
187
|
+
migrated = true;
|
|
188
|
+
}
|
|
189
|
+
if (targets.docker.engine !== "tsdocker") {
|
|
190
|
+
targets.docker.engine = "tsdocker";
|
|
191
|
+
migrated = true;
|
|
192
|
+
}
|
|
193
|
+
if (!Array.isArray(targets.docker.patterns)) {
|
|
194
|
+
targets.docker.patterns = [];
|
|
195
|
+
migrated = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
149
199
|
if (releaseConfig.changelog) {
|
|
150
200
|
delete releaseConfig.changelog;
|
|
151
201
|
migrated = true;
|
|
@@ -174,6 +224,10 @@ export const migrateSmartconfigData = (
|
|
|
174
224
|
const fromVersion = typeof cliConfig.schemaVersion === "number" ? cliConfig.schemaVersion : 1;
|
|
175
225
|
let currentVersion = fromVersion;
|
|
176
226
|
|
|
227
|
+
if (targetVersion >= 2) {
|
|
228
|
+
migrated = migrateLegacyReleaseArray(smartconfigJson) || migrated;
|
|
229
|
+
}
|
|
230
|
+
|
|
177
231
|
if (currentVersion < 2 && targetVersion >= 2) {
|
|
178
232
|
migrated = migrateToV2(smartconfigJson) || migrated;
|
|
179
233
|
currentVersion = 2;
|
package/ts/helpers.workflow.ts
CHANGED
|
@@ -52,7 +52,12 @@ export interface IReleaseNpmTargetConfig {
|
|
|
52
52
|
|
|
53
53
|
export interface IReleaseDockerTargetConfig {
|
|
54
54
|
enabled?: boolean;
|
|
55
|
-
|
|
55
|
+
engine?: "tsdocker";
|
|
56
|
+
patterns?: string[];
|
|
57
|
+
cached?: boolean;
|
|
58
|
+
parallel?: boolean | number;
|
|
59
|
+
context?: string;
|
|
60
|
+
noBuild?: boolean;
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
export interface IReleaseWorkflowConfig {
|
|
@@ -109,7 +114,12 @@ export interface IResolvedReleaseWorkflow {
|
|
|
109
114
|
npmAccessLevel: "public" | "private";
|
|
110
115
|
npmAlreadyPublished: "success" | "error";
|
|
111
116
|
dockerEnabled: boolean;
|
|
112
|
-
|
|
117
|
+
dockerEngine: "tsdocker";
|
|
118
|
+
dockerPatterns: string[];
|
|
119
|
+
dockerCached: boolean;
|
|
120
|
+
dockerParallel: boolean | number;
|
|
121
|
+
dockerContext?: string;
|
|
122
|
+
dockerNoBuild: boolean;
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
interface ICliWorkflowConfig {
|
|
@@ -382,6 +392,13 @@ export const resolveReleaseWorkflow = async (argvArg: any): Promise<IResolvedRel
|
|
|
382
392
|
npmAccessLevel: npmConfig.accessLevel || "public",
|
|
383
393
|
npmAlreadyPublished: npmConfig.alreadyPublished || "success",
|
|
384
394
|
dockerEnabled,
|
|
385
|
-
|
|
395
|
+
dockerEngine: "tsdocker",
|
|
396
|
+
dockerPatterns: Array.isArray(dockerConfig.patterns) ? dockerConfig.patterns : [],
|
|
397
|
+
dockerCached: dockerConfig.cached ?? false,
|
|
398
|
+
dockerParallel: dockerConfig.parallel ?? false,
|
|
399
|
+
dockerContext: typeof dockerConfig.context === "string" && dockerConfig.context.trim()
|
|
400
|
+
? dockerConfig.context.trim()
|
|
401
|
+
: undefined,
|
|
402
|
+
dockerNoBuild: dockerConfig.noBuild ?? false,
|
|
386
403
|
};
|
|
387
404
|
};
|
package/ts/mod_config/index.ts
CHANGED
|
@@ -168,7 +168,7 @@ async function handleInteractiveMenu(): Promise<void> {
|
|
|
168
168
|
{ name: "Configure release workflow", value: "release" },
|
|
169
169
|
{ name: "Configure services", value: "services" },
|
|
170
170
|
{ name: "Validate configuration (doctor)", value: "doctor" },
|
|
171
|
-
{ name: "Fix configuration
|
|
171
|
+
{ name: "Fix configuration", value: "fix" },
|
|
172
172
|
{ name: "Add an npm target registry", value: "add" },
|
|
173
173
|
{ name: "Remove an npm target registry", value: "remove" },
|
|
174
174
|
{ name: "Clear npm target registries", value: "clear" },
|
|
@@ -793,7 +793,7 @@ async function handleRelease(mode: ICliMode): Promise<void> {
|
|
|
793
793
|
choices: [
|
|
794
794
|
{ name: "git - push branch and tags", value: "git" },
|
|
795
795
|
{ name: "npm - publish package registries", value: "npm" },
|
|
796
|
-
{ name: "docker - build and push
|
|
796
|
+
{ name: "docker - build and push through tsdocker", value: "docker" },
|
|
797
797
|
],
|
|
798
798
|
default: getDefaultEnabledTargets(currentTargets),
|
|
799
799
|
});
|
|
@@ -860,21 +860,49 @@ async function handleRelease(mode: ICliMode): Promise<void> {
|
|
|
860
860
|
}
|
|
861
861
|
|
|
862
862
|
if (enabledTargets.includes("docker")) {
|
|
863
|
-
const
|
|
863
|
+
const patterns = await askValue<string>(interactInstance, {
|
|
864
864
|
type: "input",
|
|
865
|
-
name: "
|
|
866
|
-
message: "
|
|
867
|
-
default: Array.isArray(currentTargets.docker?.
|
|
868
|
-
? currentTargets.docker.
|
|
865
|
+
name: "dockerPatterns",
|
|
866
|
+
message: "tsdocker Dockerfile patterns (comma-separated, empty means all):",
|
|
867
|
+
default: Array.isArray(currentTargets.docker?.patterns)
|
|
868
|
+
? currentTargets.docker.patterns.join(", ")
|
|
869
869
|
: "",
|
|
870
870
|
});
|
|
871
|
+
const cached = await askValue<boolean>(interactInstance, {
|
|
872
|
+
type: "confirm",
|
|
873
|
+
name: "dockerCached",
|
|
874
|
+
message: "Use tsdocker cached builds?",
|
|
875
|
+
default: currentTargets.docker?.cached ?? false,
|
|
876
|
+
});
|
|
877
|
+
const parallel = await askValue<string>(interactInstance, {
|
|
878
|
+
type: "input",
|
|
879
|
+
name: "dockerParallel",
|
|
880
|
+
message: "tsdocker parallel mode (false, true, or concurrency number):",
|
|
881
|
+
default: formatDockerParallel(currentTargets.docker?.parallel ?? false),
|
|
882
|
+
});
|
|
883
|
+
const context = await askValue<string>(interactInstance, {
|
|
884
|
+
type: "input",
|
|
885
|
+
name: "dockerContext",
|
|
886
|
+
message: "Docker context for tsdocker (empty for default):",
|
|
887
|
+
default: currentTargets.docker?.context || "",
|
|
888
|
+
});
|
|
889
|
+
const noBuild = await askValue<boolean>(interactInstance, {
|
|
890
|
+
type: "confirm",
|
|
891
|
+
name: "dockerNoBuild",
|
|
892
|
+
message: "Skip tsdocker build and only push existing local registry images?",
|
|
893
|
+
default: currentTargets.docker?.noBuild ?? false,
|
|
894
|
+
});
|
|
871
895
|
releaseTargets.docker = {
|
|
872
|
-
...(currentTargets.docker || {}),
|
|
873
896
|
enabled: true,
|
|
874
|
-
|
|
897
|
+
engine: "tsdocker",
|
|
898
|
+
patterns: parseCsv(patterns),
|
|
899
|
+
cached,
|
|
900
|
+
parallel: parseDockerParallel(parallel),
|
|
901
|
+
context: context.trim() || undefined,
|
|
902
|
+
noBuild,
|
|
875
903
|
};
|
|
876
904
|
} else {
|
|
877
|
-
releaseTargets.docker = {
|
|
905
|
+
releaseTargets.docker = { enabled: false, engine: "tsdocker" };
|
|
878
906
|
}
|
|
879
907
|
|
|
880
908
|
setCliConfigValueInData(smartconfigData, "schemaVersion", CURRENT_GITZONE_CLI_SCHEMA_VERSION);
|
|
@@ -911,8 +939,8 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
|
|
|
911
939
|
return;
|
|
912
940
|
}
|
|
913
941
|
|
|
914
|
-
|
|
915
|
-
|
|
942
|
+
let findings = await collectDoctorFindings();
|
|
943
|
+
let counts = countDoctorFindings(findings);
|
|
916
944
|
const extraInstructions = (argvArg._?.slice(2).join(" ") || "").trim();
|
|
917
945
|
const force = Boolean(argvArg.force);
|
|
918
946
|
|
|
@@ -926,10 +954,10 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
|
|
|
926
954
|
|
|
927
955
|
if (!mode.yes) {
|
|
928
956
|
if (!mode.interactive) {
|
|
929
|
-
throw new Error("Config fix requires an interactive terminal or `-y` to run
|
|
957
|
+
throw new Error("Config fix requires an interactive terminal or `-y` to run non-interactively.");
|
|
930
958
|
}
|
|
931
959
|
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
|
|
932
|
-
`Run
|
|
960
|
+
`Run configuration fixes for .smartconfig.json? (${counts.error} error, ${counts.warn} warning)`,
|
|
933
961
|
true,
|
|
934
962
|
);
|
|
935
963
|
if (!confirmed) {
|
|
@@ -938,6 +966,16 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
|
|
|
938
966
|
}
|
|
939
967
|
}
|
|
940
968
|
|
|
969
|
+
const appliedKnownFixes = await applyKnownConfigFixes(mode);
|
|
970
|
+
if (appliedKnownFixes) {
|
|
971
|
+
findings = await collectDoctorFindings();
|
|
972
|
+
counts = countDoctorFindings(findings);
|
|
973
|
+
if (counts.error === 0 && counts.warn === 0 && !extraInstructions && !force) {
|
|
974
|
+
printDoctorResult(findings, mode);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
941
979
|
const opencodeArgs = [
|
|
942
980
|
"run",
|
|
943
981
|
"--title",
|
|
@@ -976,6 +1014,33 @@ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
|
|
|
976
1014
|
printDoctorResult(finalFindings, mode);
|
|
977
1015
|
}
|
|
978
1016
|
|
|
1017
|
+
async function applyKnownConfigFixes(mode: ICliMode): Promise<boolean> {
|
|
1018
|
+
const smartconfigPath = getSmartconfigPath();
|
|
1019
|
+
if (!(await plugins.smartfs.file(smartconfigPath).exists())) {
|
|
1020
|
+
return false;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
let smartconfigData: Record<string, any>;
|
|
1024
|
+
try {
|
|
1025
|
+
smartconfigData = await readSmartconfigFile();
|
|
1026
|
+
} catch {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const result = migrateSmartconfigData(smartconfigData);
|
|
1031
|
+
if (!result.migrated) {
|
|
1032
|
+
return false;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
await writeSmartconfigFile(smartconfigData);
|
|
1036
|
+
plugins.logger.log(
|
|
1037
|
+
"success",
|
|
1038
|
+
`Applied known .smartconfig.json migrations to schema v${result.toVersion}`,
|
|
1039
|
+
);
|
|
1040
|
+
await formatSmartconfigWithDiff(mode);
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
979
1044
|
async function collectDoctorFindings(): Promise<IDoctorFinding[]> {
|
|
980
1045
|
const findings: IDoctorFinding[] = [];
|
|
981
1046
|
const smartconfigPath = getSmartconfigPath();
|
|
@@ -1043,7 +1108,7 @@ async function collectDoctorFindings(): Promise<IDoctorFinding[]> {
|
|
|
1043
1108
|
await validateDetectedProjectType(cliConfig, findings);
|
|
1044
1109
|
|
|
1045
1110
|
validateCommitConfig(cliConfig.commit || {}, findings);
|
|
1046
|
-
await validateReleaseConfig(cliConfig.release
|
|
1111
|
+
await validateReleaseConfig(cliConfig.release, smartconfigData, findings);
|
|
1047
1112
|
|
|
1048
1113
|
return findings;
|
|
1049
1114
|
}
|
|
@@ -1291,9 +1356,14 @@ function formatTarget(enabled: unknown, targetConfig: any): string {
|
|
|
1291
1356
|
details.push(`registries=${targetConfig.registries.length}`);
|
|
1292
1357
|
}
|
|
1293
1358
|
if (targetConfig.accessLevel) details.push(`access=${targetConfig.accessLevel}`);
|
|
1294
|
-
if (
|
|
1295
|
-
|
|
1296
|
-
|
|
1359
|
+
if (targetConfig.engine) details.push(`engine=${targetConfig.engine}`);
|
|
1360
|
+
if (Array.isArray(targetConfig.patterns)) {
|
|
1361
|
+
details.push(`patterns=${targetConfig.patterns.length}`);
|
|
1362
|
+
}
|
|
1363
|
+
if (targetConfig.cached) details.push("cached=true");
|
|
1364
|
+
if (targetConfig.parallel) details.push(`parallel=${targetConfig.parallel}`);
|
|
1365
|
+
if (targetConfig.context) details.push(`context=${targetConfig.context}`);
|
|
1366
|
+
if (targetConfig.noBuild) details.push("noBuild=true");
|
|
1297
1367
|
return details.length > 0 ? `${state} (${details.join(", ")})` : state;
|
|
1298
1368
|
}
|
|
1299
1369
|
|
|
@@ -1338,6 +1408,29 @@ function parseCsv(value: string): string[] {
|
|
|
1338
1408
|
return result;
|
|
1339
1409
|
}
|
|
1340
1410
|
|
|
1411
|
+
function formatDockerParallel(value: unknown): string {
|
|
1412
|
+
if (value === true) return "true";
|
|
1413
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
1414
|
+
return String(Math.floor(value));
|
|
1415
|
+
}
|
|
1416
|
+
return "false";
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function parseDockerParallel(value: string): boolean | number {
|
|
1420
|
+
const normalizedValue = value.trim().toLowerCase();
|
|
1421
|
+
if (!normalizedValue || ["false", "no", "off", "0"].includes(normalizedValue)) {
|
|
1422
|
+
return false;
|
|
1423
|
+
}
|
|
1424
|
+
if (["true", "yes", "on"].includes(normalizedValue)) {
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
const numericValue = Number(normalizedValue);
|
|
1428
|
+
if (Number.isFinite(numericValue) && numericValue > 0) {
|
|
1429
|
+
return Math.floor(numericValue);
|
|
1430
|
+
}
|
|
1431
|
+
return false;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1341
1434
|
function normalizeRegistryUrl(url: string): string {
|
|
1342
1435
|
let normalizedUrl = url.trim();
|
|
1343
1436
|
if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
|
|
@@ -1391,6 +1484,7 @@ function buildConfigFixPrompt(
|
|
|
1391
1484
|
`- Use schemaVersion ${CURRENT_GITZONE_CLI_SCHEMA_VERSION} for ` +
|
|
1392
1485
|
"`@git.zone/cli`.",
|
|
1393
1486
|
"- Use target-based release config: `release.targets.git`, `release.targets.npm`, and `release.targets.docker`.",
|
|
1487
|
+
"- Docker release targets must use `release.targets.docker.engine = \"tsdocker\"`; Docker registries belong under `@git.zone/tsdocker`.",
|
|
1394
1488
|
"- Keep npm registries only at `@git.zone/cli.release.targets.npm.registries`.",
|
|
1395
1489
|
"- Do not add runtime legacy compatibility code. If legacy config exists, migrate it explicitly.",
|
|
1396
1490
|
"- Do not commit, release, install dependencies, or modify unrelated files.",
|
|
@@ -1513,9 +1607,24 @@ function validateCommitConfig(
|
|
|
1513
1607
|
}
|
|
1514
1608
|
|
|
1515
1609
|
async function validateReleaseConfig(
|
|
1516
|
-
|
|
1610
|
+
rawReleaseConfig: unknown,
|
|
1611
|
+
smartconfigData: Record<string, any>,
|
|
1517
1612
|
findings: IDoctorFinding[],
|
|
1518
1613
|
): Promise<void> {
|
|
1614
|
+
const releaseConfig = rawReleaseConfig === undefined ? {} : rawReleaseConfig;
|
|
1615
|
+
if (!isPlainObject(releaseConfig)) {
|
|
1616
|
+
findings.push({
|
|
1617
|
+
level: "error",
|
|
1618
|
+
message: `Release config must be an object, found ${
|
|
1619
|
+
Array.isArray(releaseConfig) ? "array" : typeof releaseConfig
|
|
1620
|
+
}`,
|
|
1621
|
+
fix: Array.isArray(releaseConfig)
|
|
1622
|
+
? "Run `gitzone config migrate` to move legacy registry arrays into release.targets.npm.registries."
|
|
1623
|
+
: "Set @git.zone/cli.release to an object or remove it.",
|
|
1624
|
+
});
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1519
1628
|
const confirmation = releaseConfig.confirmation;
|
|
1520
1629
|
if (confirmation === undefined || validConfirmationModes.includes(confirmation)) {
|
|
1521
1630
|
findings.push({ level: "ok", message: "Release confirmation mode is valid" });
|
|
@@ -1554,7 +1663,7 @@ async function validateReleaseConfig(
|
|
|
1554
1663
|
const targets = releaseConfig.targets || {};
|
|
1555
1664
|
await validateGitTarget(targets.git || {}, findings);
|
|
1556
1665
|
await validateNpmTarget(targets.npm || {}, findings);
|
|
1557
|
-
validateDockerTarget(targets.docker || {}, findings);
|
|
1666
|
+
await validateDockerTarget(targets.docker || {}, smartconfigData, findings);
|
|
1558
1667
|
}
|
|
1559
1668
|
|
|
1560
1669
|
async function validateGitTarget(
|
|
@@ -1718,29 +1827,169 @@ async function validateNpmAuth(
|
|
|
1718
1827
|
}
|
|
1719
1828
|
}
|
|
1720
1829
|
|
|
1721
|
-
function validateDockerTarget(
|
|
1830
|
+
async function validateDockerTarget(
|
|
1722
1831
|
dockerTarget: Record<string, any>,
|
|
1832
|
+
smartconfigData: Record<string, any>,
|
|
1723
1833
|
findings: IDoctorFinding[],
|
|
1724
|
-
): void {
|
|
1834
|
+
): Promise<void> {
|
|
1835
|
+
if ("images" in dockerTarget) {
|
|
1836
|
+
findings.push({
|
|
1837
|
+
level: "error",
|
|
1838
|
+
message: "Docker release target still uses removed images config",
|
|
1839
|
+
fix: "Remove release.targets.docker.images and configure @git.zone/tsdocker instead.",
|
|
1840
|
+
});
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1725
1843
|
const enabled = dockerTarget.enabled ?? false;
|
|
1726
1844
|
if (!enabled) {
|
|
1727
1845
|
findings.push({ level: "ok", message: "Docker release target is disabled" });
|
|
1728
1846
|
return;
|
|
1729
1847
|
}
|
|
1730
1848
|
|
|
1731
|
-
if (
|
|
1849
|
+
if (dockerTarget.engine !== "tsdocker") {
|
|
1732
1850
|
findings.push({
|
|
1733
1851
|
level: "error",
|
|
1734
|
-
message: "Docker release target
|
|
1735
|
-
fix: "Set release.targets.docker.
|
|
1852
|
+
message: "Docker release target must use tsdocker",
|
|
1853
|
+
fix: "Set release.targets.docker.engine to tsdocker.",
|
|
1736
1854
|
});
|
|
1737
|
-
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
if (dockerTarget.patterns !== undefined && !Array.isArray(dockerTarget.patterns)) {
|
|
1858
|
+
findings.push({
|
|
1859
|
+
level: "error",
|
|
1860
|
+
message: "Docker release target patterns must be an array",
|
|
1861
|
+
fix: "Set release.targets.docker.patterns to an array of Dockerfile patterns or remove it.",
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
if (!isValidDockerParallel(dockerTarget.parallel)) {
|
|
1866
|
+
findings.push({
|
|
1867
|
+
level: "error",
|
|
1868
|
+
message: `Invalid tsdocker parallel setting: ${formatValue(dockerTarget.parallel)}`,
|
|
1869
|
+
fix: "Use false, true, or a positive concurrency number.",
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
const tsdockerConfig = smartconfigData["@git.zone/tsdocker"];
|
|
1874
|
+
if (!isPlainObject(tsdockerConfig)) {
|
|
1875
|
+
findings.push({
|
|
1876
|
+
level: "error",
|
|
1877
|
+
message: "Docker release target is enabled but @git.zone/tsdocker config is missing",
|
|
1878
|
+
fix: "Add @git.zone/tsdocker.registries and optional registryRepoMap/platforms config.",
|
|
1879
|
+
});
|
|
1880
|
+
} else {
|
|
1881
|
+
validateTsdockerProjectConfig(tsdockerConfig, findings);
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
await validateTsdockerCommand(findings);
|
|
1885
|
+
|
|
1886
|
+
findings.push({
|
|
1887
|
+
level: "ok",
|
|
1888
|
+
message: `Docker release target uses tsdocker (${formatDockerPatterns(dockerTarget.patterns)})`,
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function formatDockerPatterns(patterns: unknown): string {
|
|
1893
|
+
return Array.isArray(patterns) && patterns.length > 0
|
|
1894
|
+
? patterns.map((pattern) => String(pattern)).join(", ")
|
|
1895
|
+
: "all Dockerfiles";
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
function isPlainObject(value: unknown): value is Record<string, any> {
|
|
1899
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
function isValidDockerParallel(value: unknown): boolean {
|
|
1903
|
+
return value === undefined ||
|
|
1904
|
+
value === false ||
|
|
1905
|
+
value === true ||
|
|
1906
|
+
(typeof value === "number" && Number.isFinite(value) && value > 0);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function validateTsdockerProjectConfig(
|
|
1910
|
+
tsdockerConfig: Record<string, any>,
|
|
1911
|
+
findings: IDoctorFinding[],
|
|
1912
|
+
): void {
|
|
1913
|
+
const registries = Array.isArray(tsdockerConfig.registries)
|
|
1914
|
+
? tsdockerConfig.registries
|
|
1915
|
+
: [];
|
|
1916
|
+
if (registries.length === 0) {
|
|
1917
|
+
findings.push({
|
|
1918
|
+
level: "error",
|
|
1919
|
+
message: "@git.zone/tsdocker.registries is empty",
|
|
1920
|
+
fix: "Set @git.zone/tsdocker.registries to registry hosts such as registry.gitlab.com.",
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
for (const registry of registries) {
|
|
1925
|
+
if (typeof registry !== "string" || !registry.trim()) {
|
|
1926
|
+
findings.push({
|
|
1927
|
+
level: "error",
|
|
1928
|
+
message: `Invalid tsdocker registry: ${formatValue(registry)}`,
|
|
1929
|
+
fix: "Use registry hosts such as registry.gitlab.com.",
|
|
1930
|
+
});
|
|
1931
|
+
continue;
|
|
1932
|
+
}
|
|
1933
|
+
if (registry.startsWith("http://") || registry.startsWith("https://")) {
|
|
1934
|
+
findings.push({
|
|
1935
|
+
level: "error",
|
|
1936
|
+
message: `tsdocker registry must not include a protocol: ${registry}`,
|
|
1937
|
+
fix: `Use ${registry.replace(/^https?:\/\//, "")}`,
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
const registryRepoMap = tsdockerConfig.registryRepoMap;
|
|
1943
|
+
if (registryRepoMap !== undefined && !isPlainObject(registryRepoMap)) {
|
|
1944
|
+
findings.push({
|
|
1945
|
+
level: "error",
|
|
1946
|
+
message: "@git.zone/tsdocker.registryRepoMap must be an object",
|
|
1947
|
+
});
|
|
1948
|
+
} else if (isPlainObject(registryRepoMap)) {
|
|
1949
|
+
for (const registry of Object.keys(registryRepoMap)) {
|
|
1950
|
+
if (registry.startsWith("http://") || registry.startsWith("https://")) {
|
|
1951
|
+
findings.push({
|
|
1952
|
+
level: "error",
|
|
1953
|
+
message: `tsdocker registryRepoMap key must not include a protocol: ${registry}`,
|
|
1954
|
+
fix: `Use ${registry.replace(/^https?:\/\//, "")}`,
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1738
1958
|
}
|
|
1739
1959
|
|
|
1740
1960
|
findings.push({
|
|
1741
1961
|
level: "ok",
|
|
1742
|
-
message:
|
|
1962
|
+
message: `@git.zone/tsdocker has ${registries.length} registries`,
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
async function validateTsdockerCommand(findings: IDoctorFinding[]): Promise<void> {
|
|
1967
|
+
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
1968
|
+
executor: "bash",
|
|
1969
|
+
sourceFilePaths: [],
|
|
1743
1970
|
});
|
|
1971
|
+
try {
|
|
1972
|
+
const result = await smartshellInstance.execSpawn(
|
|
1973
|
+
"tsdocker",
|
|
1974
|
+
["--version"],
|
|
1975
|
+
{ silent: true, timeout: 8000 },
|
|
1976
|
+
);
|
|
1977
|
+
if (result.exitCode === 0) {
|
|
1978
|
+
findings.push({ level: "ok", message: "tsdocker command is available" });
|
|
1979
|
+
} else {
|
|
1980
|
+
findings.push({
|
|
1981
|
+
level: "error",
|
|
1982
|
+
message: "tsdocker command is not available",
|
|
1983
|
+
fix: "Install @git.zone/tsdocker globally or make it available on PATH.",
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
} catch (error) {
|
|
1987
|
+
findings.push({
|
|
1988
|
+
level: "error",
|
|
1989
|
+
message: "Could not execute tsdocker",
|
|
1990
|
+
fix: error instanceof Error ? error.message : String(error),
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1744
1993
|
}
|
|
1745
1994
|
|
|
1746
1995
|
async function validateDetectedProjectType(
|
|
@@ -1795,7 +2044,7 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
1795
2044
|
{ name: "cli", description: "Configure CLI behavior interactively" },
|
|
1796
2045
|
{ name: "release", description: "Configure release workflow interactively" },
|
|
1797
2046
|
{ name: "doctor", description: "Validate .smartconfig.json" },
|
|
1798
|
-
{ name: "fix [instructions]", description: "
|
|
2047
|
+
{ name: "fix [instructions]", description: "Repair .smartconfig.json" },
|
|
1799
2048
|
{ name: "get <path>", description: "Read a single config value" },
|
|
1800
2049
|
{ name: "set <path> <value>", description: "Write a config value" },
|
|
1801
2050
|
{ name: "unset <path>", description: "Delete a config value" },
|
|
@@ -1840,7 +2089,7 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
1840
2089
|
console.log(" cli Configure CLI behavior interactively");
|
|
1841
2090
|
console.log(" release Configure release workflow interactively");
|
|
1842
2091
|
console.log(" doctor Validate .smartconfig.json");
|
|
1843
|
-
console.log(" fix [instructions]
|
|
2092
|
+
console.log(" fix [instructions] Repair .smartconfig.json");
|
|
1844
2093
|
console.log(" get <path> Read a single config value");
|
|
1845
2094
|
console.log(" set <path> <value> Write a config value");
|
|
1846
2095
|
console.log(" unset <path> Delete a config value");
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import * as plugins from './mod.plugins.js';
|
|
2
2
|
import { FormatContext } from './classes.formatcontext.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
IPlannedChange,
|
|
5
|
+
ICheckResult,
|
|
6
|
+
IFormatWarning,
|
|
7
|
+
} from './interfaces.format.js';
|
|
4
8
|
import { Project } from '../classes.project.js';
|
|
5
9
|
import { FormatStats } from './classes.formatstats.js';
|
|
6
10
|
|
|
@@ -19,6 +23,14 @@ export abstract class BaseFormatter {
|
|
|
19
23
|
abstract analyze(): Promise<IPlannedChange[]>;
|
|
20
24
|
abstract applyChange(change: IPlannedChange): Promise<void>;
|
|
21
25
|
|
|
26
|
+
get runsWithoutChanges(): boolean {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async validate(): Promise<IFormatWarning[]> {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
22
34
|
async execute(changes: IPlannedChange[]): Promise<void> {
|
|
23
35
|
const startTime = this.stats.moduleStartTime(this.name);
|
|
24
36
|
this.stats.startModule(this.name);
|