@git.zone/cli 2.16.0 → 2.17.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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/gitzone.cli.js +13 -3
- package/dist_ts/mod_config/index.js +867 -26
- package/package.json +1 -1
- package/readme.md +9 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/gitzone.cli.ts +16 -6
- package/ts/mod_config/index.ts +974 -26
package/ts/mod_config/index.ts
CHANGED
|
@@ -7,7 +7,9 @@ import { runFormatter, type ICheckResult } from "../mod_format/index.js";
|
|
|
7
7
|
import type { ICliMode } from "../helpers.climode.js";
|
|
8
8
|
import { getCliMode, printJson } from "../helpers.climode.js";
|
|
9
9
|
import {
|
|
10
|
+
CLI_NAMESPACE,
|
|
10
11
|
getCliConfigValueFromData,
|
|
12
|
+
getSmartconfigPath,
|
|
11
13
|
readSmartconfigFile,
|
|
12
14
|
setCliConfigValueInData,
|
|
13
15
|
unsetCliConfigValueInData,
|
|
@@ -100,9 +102,21 @@ export const run = async (argvArg: any) => {
|
|
|
100
102
|
case "commit":
|
|
101
103
|
await handleCommit(argvArg._?.[2], argvArg._?.[3], mode);
|
|
102
104
|
break;
|
|
105
|
+
case "project":
|
|
106
|
+
await handleProject(mode);
|
|
107
|
+
break;
|
|
108
|
+
case "cli":
|
|
109
|
+
await handleCli(mode);
|
|
110
|
+
break;
|
|
111
|
+
case "release":
|
|
112
|
+
await handleRelease(mode);
|
|
113
|
+
break;
|
|
103
114
|
case "services":
|
|
104
115
|
await handleServices(mode);
|
|
105
116
|
break;
|
|
117
|
+
case "doctor":
|
|
118
|
+
await handleDoctor(mode);
|
|
119
|
+
break;
|
|
106
120
|
case "migrate":
|
|
107
121
|
await handleMigrate(value, mode);
|
|
108
122
|
break;
|
|
@@ -145,13 +159,17 @@ async function handleInteractiveMenu(): Promise<void> {
|
|
|
145
159
|
default: "show",
|
|
146
160
|
choices: [
|
|
147
161
|
{ name: "Show current configuration", value: "show" },
|
|
162
|
+
{ name: "Configure project basics", value: "project" },
|
|
163
|
+
{ name: "Configure CLI behavior", value: "cli" },
|
|
164
|
+
{ name: "Configure commit workflow", value: "commit" },
|
|
165
|
+
{ name: "Configure release workflow", value: "release" },
|
|
166
|
+
{ name: "Configure services", value: "services" },
|
|
167
|
+
{ name: "Validate configuration (doctor)", value: "doctor" },
|
|
148
168
|
{ name: "Add an npm target registry", value: "add" },
|
|
149
169
|
{ name: "Remove an npm target registry", value: "remove" },
|
|
150
170
|
{ name: "Clear npm target registries", value: "clear" },
|
|
151
171
|
{ name: "Set access level (public/private)", value: "access" },
|
|
152
172
|
{ name: "Migrate smartconfig schema", value: "migrate" },
|
|
153
|
-
{ name: "Configure commit options", value: "commit" },
|
|
154
|
-
{ name: "Configure services", value: "services" },
|
|
155
173
|
{ name: "Show help", value: "help" },
|
|
156
174
|
],
|
|
157
175
|
});
|
|
@@ -162,6 +180,15 @@ async function handleInteractiveMenu(): Promise<void> {
|
|
|
162
180
|
case "show":
|
|
163
181
|
await handleShow(defaultCliMode);
|
|
164
182
|
break;
|
|
183
|
+
case "project":
|
|
184
|
+
await handleProject(defaultCliMode);
|
|
185
|
+
break;
|
|
186
|
+
case "cli":
|
|
187
|
+
await handleCli(defaultCliMode);
|
|
188
|
+
break;
|
|
189
|
+
case "release":
|
|
190
|
+
await handleRelease(defaultCliMode);
|
|
191
|
+
break;
|
|
165
192
|
case "add":
|
|
166
193
|
await handleAdd(undefined, defaultCliMode);
|
|
167
194
|
break;
|
|
@@ -183,6 +210,9 @@ async function handleInteractiveMenu(): Promise<void> {
|
|
|
183
210
|
case "services":
|
|
184
211
|
await handleServices(defaultCliMode);
|
|
185
212
|
break;
|
|
213
|
+
case "doctor":
|
|
214
|
+
await handleDoctor(defaultCliMode);
|
|
215
|
+
break;
|
|
186
216
|
case "help":
|
|
187
217
|
showHelp();
|
|
188
218
|
break;
|
|
@@ -190,48 +220,80 @@ async function handleInteractiveMenu(): Promise<void> {
|
|
|
190
220
|
}
|
|
191
221
|
|
|
192
222
|
/**
|
|
193
|
-
* Show current
|
|
223
|
+
* Show current CLI project configuration
|
|
194
224
|
*/
|
|
195
225
|
async function handleShow(mode: ICliMode): Promise<void> {
|
|
226
|
+
const smartconfigData = await readSmartconfigFile();
|
|
227
|
+
const cliConfig = getCliConfigValueFromData(smartconfigData, "") || {};
|
|
228
|
+
|
|
196
229
|
if (mode.json) {
|
|
197
|
-
|
|
198
|
-
printJson(getCliConfigValueFromData(smartconfigData, ""));
|
|
230
|
+
printJson(cliConfig);
|
|
199
231
|
return;
|
|
200
232
|
}
|
|
201
233
|
|
|
202
|
-
const config = await ReleaseConfig.fromCwd();
|
|
203
|
-
const registries = config.getRegistries();
|
|
204
|
-
const accessLevel = config.getAccessLevel();
|
|
205
|
-
|
|
206
234
|
console.log("");
|
|
207
235
|
console.log(
|
|
208
236
|
"╭─────────────────────────────────────────────────────────────╮",
|
|
209
237
|
);
|
|
210
238
|
console.log(
|
|
211
|
-
"│
|
|
239
|
+
"│ gitzone config - Project Configuration │",
|
|
212
240
|
);
|
|
213
241
|
console.log(
|
|
214
242
|
"╰─────────────────────────────────────────────────────────────╯",
|
|
215
243
|
);
|
|
216
244
|
console.log("");
|
|
217
245
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (registries.length === 0) {
|
|
223
|
-
plugins.logger.log("info", "No npm target registries configured.");
|
|
224
|
-
console.log("");
|
|
225
|
-
console.log(" Run `gitzone config add <registry-url>` to add one.");
|
|
226
|
-
console.log("");
|
|
227
|
-
} else {
|
|
228
|
-
plugins.logger.log("info", `Configured npm target registries (${registries.length}):`);
|
|
229
|
-
console.log("");
|
|
230
|
-
registries.forEach((url, index) => {
|
|
231
|
-
console.log(` ${index + 1}. ${url}`);
|
|
232
|
-
});
|
|
246
|
+
if (Object.keys(cliConfig).length === 0) {
|
|
247
|
+
plugins.logger.log("warn", `No ${CLI_NAMESPACE} configuration found.`);
|
|
248
|
+
console.log(" Run `gitzone config project` to create project basics.");
|
|
233
249
|
console.log("");
|
|
250
|
+
return;
|
|
234
251
|
}
|
|
252
|
+
|
|
253
|
+
printConfigSection("Project", [
|
|
254
|
+
["schemaVersion", formatValue(cliConfig.schemaVersion)],
|
|
255
|
+
["projectType", formatValue(cliConfig.projectType)],
|
|
256
|
+
["repository", formatRepository(cliConfig.module)],
|
|
257
|
+
["description", formatValue(cliConfig.module?.description)],
|
|
258
|
+
["npm package", formatValue(cliConfig.module?.npmPackagename || cliConfig.module?.npmPackageName)],
|
|
259
|
+
["license", formatValue(cliConfig.module?.license)],
|
|
260
|
+
["keywords", formatList(cliConfig.module?.keywords)],
|
|
261
|
+
]);
|
|
262
|
+
|
|
263
|
+
printConfigSection("CLI Behavior", [
|
|
264
|
+
["interactive", formatValue(cliConfig.cli?.interactive)],
|
|
265
|
+
["output", formatValue(cliConfig.cli?.output)],
|
|
266
|
+
["checkUpdates", formatValue(cliConfig.cli?.checkUpdates)],
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
printConfigSection("Commit Workflow", [
|
|
270
|
+
["confirmation", formatValue(cliConfig.commit?.confirmation)],
|
|
271
|
+
["steps", formatList(cliConfig.commit?.steps)],
|
|
272
|
+
["test command", formatValue(cliConfig.commit?.test?.command)],
|
|
273
|
+
["build command", formatValue(cliConfig.commit?.build?.command)],
|
|
274
|
+
["push remote", formatValue(cliConfig.commit?.push?.remote)],
|
|
275
|
+
["push followTags", formatValue(cliConfig.commit?.push?.followTags)],
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
const release = cliConfig.release || {};
|
|
279
|
+
const targets = release.targets || {};
|
|
280
|
+
printConfigSection("Release Workflow", [
|
|
281
|
+
["confirmation", formatValue(release.confirmation)],
|
|
282
|
+
["require clean tree", formatValue(release.preflight?.requireCleanTree)],
|
|
283
|
+
["run tests", formatValue(release.preflight?.test)],
|
|
284
|
+
["run build", formatValue(release.preflight?.build)],
|
|
285
|
+
["test command", formatValue(release.preflight?.testCommand)],
|
|
286
|
+
["build command", formatValue(release.preflight?.buildCommand)],
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
printConfigSection("Release Targets", [
|
|
290
|
+
["git", formatTarget(targets.git?.enabled, targets.git)],
|
|
291
|
+
["npm", formatTarget(targets.npm?.enabled, targets.npm)],
|
|
292
|
+
["docker", formatTarget(targets.docker?.enabled, targets.docker)],
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
console.log("Run `gitzone config doctor` to validate this configuration.");
|
|
296
|
+
console.log("");
|
|
235
297
|
}
|
|
236
298
|
|
|
237
299
|
/**
|
|
@@ -251,7 +313,7 @@ async function handleAdd(
|
|
|
251
313
|
const response = await interactInstance.askQuestion({
|
|
252
314
|
type: "input",
|
|
253
315
|
name: "registryUrl",
|
|
254
|
-
|
|
316
|
+
message: "Enter npm target registry URL:",
|
|
255
317
|
default: "https://registry.npmjs.org",
|
|
256
318
|
validate: (input: string) => {
|
|
257
319
|
return !!(input && input.trim() !== "");
|
|
@@ -547,6 +609,358 @@ function showCommitHelp(): void {
|
|
|
547
609
|
console.log("");
|
|
548
610
|
}
|
|
549
611
|
|
|
612
|
+
async function handleProject(mode: ICliMode): Promise<void> {
|
|
613
|
+
if (!mode.interactive) {
|
|
614
|
+
throw new Error("Project configuration requires interactive mode. Use `gitzone config set` for automation.");
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const smartconfigData = await readSmartconfigFile();
|
|
618
|
+
const cliConfig = getCliConfigValueFromData(smartconfigData, "") || {};
|
|
619
|
+
const moduleConfig = cliConfig.module || {};
|
|
620
|
+
const packageJson = await readPackageJson();
|
|
621
|
+
const interactInstance = new plugins.smartinteract.SmartInteract();
|
|
622
|
+
|
|
623
|
+
const projectType = await askValue<string>(interactInstance, {
|
|
624
|
+
type: "list",
|
|
625
|
+
name: "projectType",
|
|
626
|
+
message: "What kind of project is this?",
|
|
627
|
+
choices: ["npm", "service", "wcc", "website"],
|
|
628
|
+
default: cliConfig.projectType || "npm",
|
|
629
|
+
});
|
|
630
|
+
const githost = await askValue<string>(interactInstance, {
|
|
631
|
+
type: "input",
|
|
632
|
+
name: "githost",
|
|
633
|
+
message: "Git host:",
|
|
634
|
+
default: moduleConfig.githost || "code.foss.global",
|
|
635
|
+
});
|
|
636
|
+
const gitscope = await askValue<string>(interactInstance, {
|
|
637
|
+
type: "input",
|
|
638
|
+
name: "gitscope",
|
|
639
|
+
message: "Git scope/owner:",
|
|
640
|
+
default: moduleConfig.gitscope || "git.zone",
|
|
641
|
+
});
|
|
642
|
+
const gitrepo = await askValue<string>(interactInstance, {
|
|
643
|
+
type: "input",
|
|
644
|
+
name: "gitrepo",
|
|
645
|
+
message: "Git repository name:",
|
|
646
|
+
default: moduleConfig.gitrepo || inferRepoName(packageJson.name),
|
|
647
|
+
});
|
|
648
|
+
const description = await askValue<string>(interactInstance, {
|
|
649
|
+
type: "input",
|
|
650
|
+
name: "description",
|
|
651
|
+
message: "Project description:",
|
|
652
|
+
default: moduleConfig.description || packageJson.description || "",
|
|
653
|
+
});
|
|
654
|
+
const npmPackagename = await askValue<string>(interactInstance, {
|
|
655
|
+
type: "input",
|
|
656
|
+
name: "npmPackagename",
|
|
657
|
+
message: "npm package name:",
|
|
658
|
+
default: moduleConfig.npmPackagename || moduleConfig.npmPackageName || packageJson.name || "",
|
|
659
|
+
});
|
|
660
|
+
const license = await askValue<string>(interactInstance, {
|
|
661
|
+
type: "input",
|
|
662
|
+
name: "license",
|
|
663
|
+
message: "License:",
|
|
664
|
+
default: moduleConfig.license || packageJson.license || "MIT",
|
|
665
|
+
});
|
|
666
|
+
const keywords = await askValue<string>(interactInstance, {
|
|
667
|
+
type: "input",
|
|
668
|
+
name: "keywords",
|
|
669
|
+
message: "Keywords (comma-separated):",
|
|
670
|
+
default: Array.isArray(moduleConfig.keywords)
|
|
671
|
+
? moduleConfig.keywords.join(", ")
|
|
672
|
+
: Array.isArray(packageJson.keywords)
|
|
673
|
+
? packageJson.keywords.join(", ")
|
|
674
|
+
: "",
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
setCliConfigValueInData(smartconfigData, "schemaVersion", CURRENT_GITZONE_CLI_SCHEMA_VERSION);
|
|
678
|
+
setCliConfigValueInData(smartconfigData, "projectType", projectType);
|
|
679
|
+
setCliConfigValueInData(smartconfigData, "module", {
|
|
680
|
+
...moduleConfig,
|
|
681
|
+
githost: githost.trim(),
|
|
682
|
+
gitscope: gitscope.trim(),
|
|
683
|
+
gitrepo: gitrepo.trim(),
|
|
684
|
+
description: description.trim(),
|
|
685
|
+
npmPackagename: npmPackagename.trim(),
|
|
686
|
+
license: license.trim(),
|
|
687
|
+
keywords: parseCsv(keywords),
|
|
688
|
+
});
|
|
689
|
+
await writeSmartconfigFile(smartconfigData);
|
|
690
|
+
plugins.logger.log("success", "Project configuration updated");
|
|
691
|
+
await formatSmartconfigWithDiff(mode);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function handleCli(mode: ICliMode): Promise<void> {
|
|
695
|
+
if (!mode.interactive) {
|
|
696
|
+
throw new Error("CLI behavior configuration requires interactive mode. Use `gitzone config set` for automation.");
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const smartconfigData = await readSmartconfigFile();
|
|
700
|
+
const cliConfig = getCliConfigValueFromData(smartconfigData, "cli") || {};
|
|
701
|
+
const interactInstance = new plugins.smartinteract.SmartInteract();
|
|
702
|
+
|
|
703
|
+
const output = await askValue<string>(interactInstance, {
|
|
704
|
+
type: "list",
|
|
705
|
+
name: "output",
|
|
706
|
+
message: "Default output mode:",
|
|
707
|
+
choices: ["human", "plain", "json"],
|
|
708
|
+
default: cliConfig.output || "human",
|
|
709
|
+
});
|
|
710
|
+
const interactive = await askValue<boolean>(interactInstance, {
|
|
711
|
+
type: "confirm",
|
|
712
|
+
name: "interactive",
|
|
713
|
+
message: "Enable interactive prompts by default?",
|
|
714
|
+
default: cliConfig.interactive ?? true,
|
|
715
|
+
});
|
|
716
|
+
const checkUpdates = await askValue<boolean>(interactInstance, {
|
|
717
|
+
type: "confirm",
|
|
718
|
+
name: "checkUpdates",
|
|
719
|
+
message: "Check for gitzone updates in human mode?",
|
|
720
|
+
default: cliConfig.checkUpdates ?? true,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
setCliConfigValueInData(smartconfigData, "schemaVersion", CURRENT_GITZONE_CLI_SCHEMA_VERSION);
|
|
724
|
+
setCliConfigValueInData(smartconfigData, "cli", {
|
|
725
|
+
output,
|
|
726
|
+
interactive,
|
|
727
|
+
checkUpdates,
|
|
728
|
+
});
|
|
729
|
+
await writeSmartconfigFile(smartconfigData);
|
|
730
|
+
plugins.logger.log("success", "CLI behavior configuration updated");
|
|
731
|
+
await formatSmartconfigWithDiff(mode);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
async function handleRelease(mode: ICliMode): Promise<void> {
|
|
735
|
+
if (!mode.interactive) {
|
|
736
|
+
throw new Error("Release configuration requires interactive mode. Use `gitzone config set` for automation.");
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const smartconfigData = await readSmartconfigFile();
|
|
740
|
+
const currentRelease = getCliConfigValueFromData(smartconfigData, "release") || {};
|
|
741
|
+
const currentTargets = currentRelease.targets || {};
|
|
742
|
+
const interactInstance = new plugins.smartinteract.SmartInteract();
|
|
743
|
+
|
|
744
|
+
const confirmation = await askValue<string>(interactInstance, {
|
|
745
|
+
type: "list",
|
|
746
|
+
name: "confirmation",
|
|
747
|
+
message: "Release confirmation mode:",
|
|
748
|
+
choices: ["prompt", "auto", "plan"],
|
|
749
|
+
default: currentRelease.confirmation || "prompt",
|
|
750
|
+
});
|
|
751
|
+
const requireCleanTree = await askValue<boolean>(interactInstance, {
|
|
752
|
+
type: "confirm",
|
|
753
|
+
name: "requireCleanTree",
|
|
754
|
+
message: "Require a clean git tree before release?",
|
|
755
|
+
default: currentRelease.preflight?.requireCleanTree ?? true,
|
|
756
|
+
});
|
|
757
|
+
const runTests = await askValue<boolean>(interactInstance, {
|
|
758
|
+
type: "confirm",
|
|
759
|
+
name: "runTests",
|
|
760
|
+
message: "Run tests during release preflight?",
|
|
761
|
+
default: currentRelease.preflight?.test ?? false,
|
|
762
|
+
});
|
|
763
|
+
const runBuild = await askValue<boolean>(interactInstance, {
|
|
764
|
+
type: "confirm",
|
|
765
|
+
name: "runBuild",
|
|
766
|
+
message: "Run build during release?",
|
|
767
|
+
default: currentRelease.preflight?.build ?? true,
|
|
768
|
+
});
|
|
769
|
+
const testCommand = await askValue<string>(interactInstance, {
|
|
770
|
+
type: "input",
|
|
771
|
+
name: "testCommand",
|
|
772
|
+
message: "Release test command:",
|
|
773
|
+
default: currentRelease.preflight?.testCommand || "pnpm test",
|
|
774
|
+
});
|
|
775
|
+
const buildCommand = await askValue<string>(interactInstance, {
|
|
776
|
+
type: "input",
|
|
777
|
+
name: "buildCommand",
|
|
778
|
+
message: "Release build command:",
|
|
779
|
+
default: currentRelease.preflight?.buildCommand || "pnpm build",
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
const enabledTargets = await askValue<string[]>(interactInstance, {
|
|
783
|
+
type: "checkbox",
|
|
784
|
+
name: "targets",
|
|
785
|
+
message: "Enable release targets:",
|
|
786
|
+
choices: [
|
|
787
|
+
{ name: "git - push branch and tags", value: "git" },
|
|
788
|
+
{ name: "npm - publish package registries", value: "npm" },
|
|
789
|
+
{ name: "docker - build and push images", value: "docker" },
|
|
790
|
+
],
|
|
791
|
+
default: getDefaultEnabledTargets(currentTargets),
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
const releaseTargets: Record<string, any> = { ...currentTargets };
|
|
795
|
+
|
|
796
|
+
if (enabledTargets.includes("git")) {
|
|
797
|
+
releaseTargets.git = {
|
|
798
|
+
...(currentTargets.git || {}),
|
|
799
|
+
enabled: true,
|
|
800
|
+
remote: await askValue<string>(interactInstance, {
|
|
801
|
+
type: "input",
|
|
802
|
+
name: "gitRemote",
|
|
803
|
+
message: "Git remote:",
|
|
804
|
+
default: currentTargets.git?.remote || "origin",
|
|
805
|
+
}),
|
|
806
|
+
pushBranch: await askValue<boolean>(interactInstance, {
|
|
807
|
+
type: "confirm",
|
|
808
|
+
name: "pushBranch",
|
|
809
|
+
message: "Push release commit branch?",
|
|
810
|
+
default: currentTargets.git?.pushBranch ?? true,
|
|
811
|
+
}),
|
|
812
|
+
pushTags: await askValue<boolean>(interactInstance, {
|
|
813
|
+
type: "confirm",
|
|
814
|
+
name: "pushTags",
|
|
815
|
+
message: "Push release tags?",
|
|
816
|
+
default: currentTargets.git?.pushTags ?? true,
|
|
817
|
+
}),
|
|
818
|
+
};
|
|
819
|
+
} else {
|
|
820
|
+
releaseTargets.git = { ...(currentTargets.git || {}), enabled: false };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (enabledTargets.includes("npm")) {
|
|
824
|
+
const registries = await askValue<string>(interactInstance, {
|
|
825
|
+
type: "input",
|
|
826
|
+
name: "npmRegistries",
|
|
827
|
+
message: "npm registries (comma-separated):",
|
|
828
|
+
default: Array.isArray(currentTargets.npm?.registries)
|
|
829
|
+
? currentTargets.npm.registries.join(", ")
|
|
830
|
+
: "https://registry.npmjs.org",
|
|
831
|
+
});
|
|
832
|
+
releaseTargets.npm = {
|
|
833
|
+
...(currentTargets.npm || {}),
|
|
834
|
+
enabled: true,
|
|
835
|
+
registries: parseCsv(registries).map(normalizeRegistryUrl),
|
|
836
|
+
accessLevel: await askValue<string>(interactInstance, {
|
|
837
|
+
type: "list",
|
|
838
|
+
name: "npmAccessLevel",
|
|
839
|
+
message: "npm publish access level:",
|
|
840
|
+
choices: ["public", "private"],
|
|
841
|
+
default: currentTargets.npm?.accessLevel || "public",
|
|
842
|
+
}),
|
|
843
|
+
alreadyPublished: await askValue<string>(interactInstance, {
|
|
844
|
+
type: "list",
|
|
845
|
+
name: "alreadyPublished",
|
|
846
|
+
message: "When a package version is already published:",
|
|
847
|
+
choices: ["success", "error"],
|
|
848
|
+
default: currentTargets.npm?.alreadyPublished || "success",
|
|
849
|
+
}),
|
|
850
|
+
};
|
|
851
|
+
} else {
|
|
852
|
+
releaseTargets.npm = { ...(currentTargets.npm || {}), enabled: false };
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (enabledTargets.includes("docker")) {
|
|
856
|
+
const images = await askValue<string>(interactInstance, {
|
|
857
|
+
type: "input",
|
|
858
|
+
name: "dockerImages",
|
|
859
|
+
message: "Docker image templates (comma-separated, supports {{version}}):",
|
|
860
|
+
default: Array.isArray(currentTargets.docker?.images)
|
|
861
|
+
? currentTargets.docker.images.join(", ")
|
|
862
|
+
: "",
|
|
863
|
+
});
|
|
864
|
+
releaseTargets.docker = {
|
|
865
|
+
...(currentTargets.docker || {}),
|
|
866
|
+
enabled: true,
|
|
867
|
+
images: parseCsv(images),
|
|
868
|
+
};
|
|
869
|
+
} else {
|
|
870
|
+
releaseTargets.docker = { ...(currentTargets.docker || {}), enabled: false };
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
setCliConfigValueInData(smartconfigData, "schemaVersion", CURRENT_GITZONE_CLI_SCHEMA_VERSION);
|
|
874
|
+
setCliConfigValueInData(smartconfigData, "release", {
|
|
875
|
+
...currentRelease,
|
|
876
|
+
confirmation,
|
|
877
|
+
preflight: {
|
|
878
|
+
...(currentRelease.preflight || {}),
|
|
879
|
+
requireCleanTree,
|
|
880
|
+
test: runTests,
|
|
881
|
+
build: runBuild,
|
|
882
|
+
testCommand: testCommand.trim(),
|
|
883
|
+
buildCommand: buildCommand.trim(),
|
|
884
|
+
},
|
|
885
|
+
targets: releaseTargets,
|
|
886
|
+
});
|
|
887
|
+
await writeSmartconfigFile(smartconfigData);
|
|
888
|
+
plugins.logger.log("success", "Release configuration updated");
|
|
889
|
+
await formatSmartconfigWithDiff(mode);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
async function handleDoctor(mode: ICliMode): Promise<void> {
|
|
893
|
+
const findings: IDoctorFinding[] = [];
|
|
894
|
+
const smartconfigPath = getSmartconfigPath();
|
|
895
|
+
const smartconfigExists = await plugins.smartfs.file(smartconfigPath).exists();
|
|
896
|
+
|
|
897
|
+
if (!smartconfigExists) {
|
|
898
|
+
findings.push({
|
|
899
|
+
level: "warn",
|
|
900
|
+
message: ".smartconfig.json does not exist",
|
|
901
|
+
fix: "Run `gitzone config project` to create project basics.",
|
|
902
|
+
});
|
|
903
|
+
return printDoctorResult(findings, mode);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
let smartconfigData: Record<string, any>;
|
|
907
|
+
try {
|
|
908
|
+
smartconfigData = await readSmartconfigFile();
|
|
909
|
+
} catch (error) {
|
|
910
|
+
findings.push({
|
|
911
|
+
level: "error",
|
|
912
|
+
message: ".smartconfig.json is not valid JSON",
|
|
913
|
+
fix: error instanceof Error ? error.message : String(error),
|
|
914
|
+
});
|
|
915
|
+
return printDoctorResult(findings, mode);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const cliConfig = getCliConfigValueFromData(smartconfigData, "") || {};
|
|
919
|
+
if (Object.keys(cliConfig).length === 0) {
|
|
920
|
+
findings.push({
|
|
921
|
+
level: "error",
|
|
922
|
+
message: `${CLI_NAMESPACE} configuration is missing`,
|
|
923
|
+
fix: "Run `gitzone config project` or `gitzone config migrate`.",
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
for (const legacyNamespace of ["gitzone", "tsdoc", "npmdocker", "npmci", "szci"]) {
|
|
928
|
+
if (smartconfigData[legacyNamespace]) {
|
|
929
|
+
findings.push({
|
|
930
|
+
level: "warn",
|
|
931
|
+
message: `Legacy namespace '${legacyNamespace}' is present`,
|
|
932
|
+
fix: "Run `gitzone config migrate`.",
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (cliConfig.schemaVersion === CURRENT_GITZONE_CLI_SCHEMA_VERSION) {
|
|
938
|
+
findings.push({ level: "ok", message: `Schema version is current (${CURRENT_GITZONE_CLI_SCHEMA_VERSION})` });
|
|
939
|
+
} else {
|
|
940
|
+
findings.push({
|
|
941
|
+
level: "warn",
|
|
942
|
+
message: `Schema version is ${formatValue(cliConfig.schemaVersion)}, expected ${CURRENT_GITZONE_CLI_SCHEMA_VERSION}`,
|
|
943
|
+
fix: "Run `gitzone config migrate`.",
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (["npm", "service", "wcc", "website"].includes(cliConfig.projectType)) {
|
|
948
|
+
findings.push({ level: "ok", message: `Project type is ${cliConfig.projectType}` });
|
|
949
|
+
} else {
|
|
950
|
+
findings.push({
|
|
951
|
+
level: "warn",
|
|
952
|
+
message: `Project type is missing or invalid: ${formatValue(cliConfig.projectType)}`,
|
|
953
|
+
fix: "Run `gitzone config project`.",
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
await validateDetectedProjectType(cliConfig, findings);
|
|
957
|
+
|
|
958
|
+
validateCommitConfig(cliConfig.commit || {}, findings);
|
|
959
|
+
await validateReleaseConfig(cliConfig.release || {}, findings);
|
|
960
|
+
|
|
961
|
+
printDoctorResult(findings, mode);
|
|
962
|
+
}
|
|
963
|
+
|
|
550
964
|
/**
|
|
551
965
|
* Handle services configuration
|
|
552
966
|
*/
|
|
@@ -715,6 +1129,526 @@ function parseConfigValue(rawValue: string): any {
|
|
|
715
1129
|
return rawValue;
|
|
716
1130
|
}
|
|
717
1131
|
|
|
1132
|
+
type TDoctorFindingLevel = "ok" | "warn" | "error";
|
|
1133
|
+
|
|
1134
|
+
interface IDoctorFinding {
|
|
1135
|
+
level: TDoctorFindingLevel;
|
|
1136
|
+
message: string;
|
|
1137
|
+
fix?: string;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const validProjectTypes = ["npm", "service", "wcc", "website"];
|
|
1141
|
+
const validConfirmationModes = ["prompt", "auto", "plan"];
|
|
1142
|
+
const validCommitSteps = [
|
|
1143
|
+
"format",
|
|
1144
|
+
"analyze",
|
|
1145
|
+
"test",
|
|
1146
|
+
"build",
|
|
1147
|
+
"changelog",
|
|
1148
|
+
"commit",
|
|
1149
|
+
"push",
|
|
1150
|
+
];
|
|
1151
|
+
|
|
1152
|
+
function printConfigSection(
|
|
1153
|
+
title: string,
|
|
1154
|
+
rows: Array<[string, string]>,
|
|
1155
|
+
): void {
|
|
1156
|
+
console.log(`${title}:`);
|
|
1157
|
+
for (const [label, value] of rows) {
|
|
1158
|
+
console.log(` ${label.padEnd(20)} ${value}`);
|
|
1159
|
+
}
|
|
1160
|
+
console.log("");
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function formatValue(value: unknown): string {
|
|
1164
|
+
if (value === undefined || value === null || value === "") {
|
|
1165
|
+
return "(unset)";
|
|
1166
|
+
}
|
|
1167
|
+
if (typeof value === "boolean") {
|
|
1168
|
+
return value ? "true" : "false";
|
|
1169
|
+
}
|
|
1170
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
1171
|
+
return String(value);
|
|
1172
|
+
}
|
|
1173
|
+
return JSON.stringify(value);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function formatRepository(moduleConfig: any): string {
|
|
1177
|
+
if (!moduleConfig) {
|
|
1178
|
+
return "(unset)";
|
|
1179
|
+
}
|
|
1180
|
+
const parts = [
|
|
1181
|
+
moduleConfig.githost,
|
|
1182
|
+
moduleConfig.gitscope,
|
|
1183
|
+
moduleConfig.gitrepo,
|
|
1184
|
+
].filter(Boolean);
|
|
1185
|
+
return parts.length > 0 ? parts.join("/") : "(unset)";
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function formatList(value: unknown): string {
|
|
1189
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1190
|
+
return "(none)";
|
|
1191
|
+
}
|
|
1192
|
+
return value.map((item) => String(item)).join(", ");
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function formatTarget(enabled: unknown, targetConfig: any): string {
|
|
1196
|
+
const state = enabled === false ? "disabled" : "enabled";
|
|
1197
|
+
if (!targetConfig || Object.keys(targetConfig).length === 0) {
|
|
1198
|
+
return state;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const details: string[] = [];
|
|
1202
|
+
if (targetConfig.remote) details.push(`remote=${targetConfig.remote}`);
|
|
1203
|
+
if (Array.isArray(targetConfig.registries)) {
|
|
1204
|
+
details.push(`registries=${targetConfig.registries.length}`);
|
|
1205
|
+
}
|
|
1206
|
+
if (targetConfig.accessLevel) details.push(`access=${targetConfig.accessLevel}`);
|
|
1207
|
+
if (Array.isArray(targetConfig.images)) {
|
|
1208
|
+
details.push(`images=${targetConfig.images.length}`);
|
|
1209
|
+
}
|
|
1210
|
+
return details.length > 0 ? `${state} (${details.join(", ")})` : state;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
async function askValue<T>(
|
|
1214
|
+
interactInstance: any,
|
|
1215
|
+
options: any,
|
|
1216
|
+
): Promise<T> {
|
|
1217
|
+
const response = await interactInstance.askQuestion(options);
|
|
1218
|
+
return response.value as T;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
async function readPackageJson(): Promise<Record<string, any>> {
|
|
1222
|
+
const packageJsonPath = plugins.path.join(process.cwd(), "package.json");
|
|
1223
|
+
if (!(await plugins.smartfs.file(packageJsonPath).exists())) {
|
|
1224
|
+
return {};
|
|
1225
|
+
}
|
|
1226
|
+
const content = (await plugins.smartfs
|
|
1227
|
+
.file(packageJsonPath)
|
|
1228
|
+
.encoding("utf8")
|
|
1229
|
+
.read()) as string;
|
|
1230
|
+
return JSON.parse(content);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function inferRepoName(packageName: unknown): string {
|
|
1234
|
+
if (typeof packageName === "string" && packageName.trim()) {
|
|
1235
|
+
const normalizedName = packageName.trim();
|
|
1236
|
+
return normalizedName.includes("/")
|
|
1237
|
+
? normalizedName.split("/").pop() || normalizedName
|
|
1238
|
+
: normalizedName;
|
|
1239
|
+
}
|
|
1240
|
+
return plugins.path.basename(process.cwd());
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function parseCsv(value: string): string[] {
|
|
1244
|
+
const result: string[] = [];
|
|
1245
|
+
for (const item of value.split(",")) {
|
|
1246
|
+
const trimmedItem = item.trim();
|
|
1247
|
+
if (trimmedItem && !result.includes(trimmedItem)) {
|
|
1248
|
+
result.push(trimmedItem);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return result;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function normalizeRegistryUrl(url: string): string {
|
|
1255
|
+
let normalizedUrl = url.trim();
|
|
1256
|
+
if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
|
|
1257
|
+
normalizedUrl = `https://${normalizedUrl}`;
|
|
1258
|
+
}
|
|
1259
|
+
return normalizedUrl.endsWith("/") ? normalizedUrl.slice(0, -1) : normalizedUrl;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function getDefaultEnabledTargets(currentTargets: Record<string, any>): string[] {
|
|
1263
|
+
const enabledTargets: string[] = [];
|
|
1264
|
+
const npmRegistries = currentTargets.npm?.registries;
|
|
1265
|
+
if (currentTargets.git?.enabled ?? true) {
|
|
1266
|
+
enabledTargets.push("git");
|
|
1267
|
+
}
|
|
1268
|
+
if (currentTargets.npm?.enabled ?? (Array.isArray(npmRegistries) && npmRegistries.length > 0)) {
|
|
1269
|
+
enabledTargets.push("npm");
|
|
1270
|
+
}
|
|
1271
|
+
if (currentTargets.docker?.enabled ?? false) {
|
|
1272
|
+
enabledTargets.push("docker");
|
|
1273
|
+
}
|
|
1274
|
+
return enabledTargets;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function printDoctorResult(findings: IDoctorFinding[], mode: ICliMode): void {
|
|
1278
|
+
const counts = findings.reduce(
|
|
1279
|
+
(accumulator, finding) => {
|
|
1280
|
+
accumulator[finding.level] += 1;
|
|
1281
|
+
return accumulator;
|
|
1282
|
+
},
|
|
1283
|
+
{ ok: 0, warn: 0, error: 0 } as Record<TDoctorFindingLevel, number>,
|
|
1284
|
+
);
|
|
1285
|
+
|
|
1286
|
+
if (mode.json) {
|
|
1287
|
+
printJson({
|
|
1288
|
+
ok: counts.error === 0,
|
|
1289
|
+
counts,
|
|
1290
|
+
findings,
|
|
1291
|
+
});
|
|
1292
|
+
} else {
|
|
1293
|
+
console.log("");
|
|
1294
|
+
console.log("gitzone config doctor");
|
|
1295
|
+
console.log("");
|
|
1296
|
+
for (const finding of findings) {
|
|
1297
|
+
const prefix = finding.level.toUpperCase().padEnd(5);
|
|
1298
|
+
console.log(`${prefix} ${finding.message}`);
|
|
1299
|
+
if (finding.fix) {
|
|
1300
|
+
console.log(` ${finding.fix}`);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
console.log("");
|
|
1304
|
+
console.log(`Summary: ${counts.ok} ok, ${counts.warn} warning, ${counts.error} error`);
|
|
1305
|
+
console.log("");
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
if (counts.error > 0) {
|
|
1309
|
+
process.exitCode = 1;
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function validateCommitConfig(
|
|
1314
|
+
commitConfig: Record<string, any>,
|
|
1315
|
+
findings: IDoctorFinding[],
|
|
1316
|
+
): void {
|
|
1317
|
+
const confirmation = commitConfig.confirmation;
|
|
1318
|
+
if (confirmation === undefined || validConfirmationModes.includes(confirmation)) {
|
|
1319
|
+
findings.push({ level: "ok", message: "Commit confirmation mode is valid" });
|
|
1320
|
+
} else {
|
|
1321
|
+
findings.push({
|
|
1322
|
+
level: "warn",
|
|
1323
|
+
message: `Invalid commit confirmation mode: ${formatValue(confirmation)}`,
|
|
1324
|
+
fix: "Use prompt, auto, or plan.",
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const steps = commitConfig.steps;
|
|
1329
|
+
if (steps === undefined) {
|
|
1330
|
+
findings.push({ level: "ok", message: "Commit workflow uses default steps" });
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
1334
|
+
findings.push({
|
|
1335
|
+
level: "error",
|
|
1336
|
+
message: "Commit steps must be a non-empty array",
|
|
1337
|
+
fix: "Run `gitzone config commit` or unset commit.steps.",
|
|
1338
|
+
});
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const invalidSteps = steps.filter((step) => !validCommitSteps.includes(step));
|
|
1343
|
+
if (invalidSteps.length > 0) {
|
|
1344
|
+
findings.push({
|
|
1345
|
+
level: "error",
|
|
1346
|
+
message: `Invalid commit steps: ${invalidSteps.join(", ")}`,
|
|
1347
|
+
fix: `Allowed steps: ${validCommitSteps.join(", ")}`,
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const analyzeIndex = steps.indexOf("analyze");
|
|
1352
|
+
const changelogIndex = steps.indexOf("changelog");
|
|
1353
|
+
const commitIndex = steps.indexOf("commit");
|
|
1354
|
+
if (analyzeIndex === -1 || changelogIndex === -1 || commitIndex === -1) {
|
|
1355
|
+
findings.push({
|
|
1356
|
+
level: "error",
|
|
1357
|
+
message: "Commit workflow must include analyze, changelog, and commit",
|
|
1358
|
+
fix: "Run `gitzone config commit` or reset commit.steps.",
|
|
1359
|
+
});
|
|
1360
|
+
} else if (analyzeIndex > commitIndex || changelogIndex > commitIndex) {
|
|
1361
|
+
findings.push({
|
|
1362
|
+
level: "error",
|
|
1363
|
+
message: "Commit workflow must run analyze and changelog before commit",
|
|
1364
|
+
fix: "Move analyze and changelog before commit in commit.steps.",
|
|
1365
|
+
});
|
|
1366
|
+
} else {
|
|
1367
|
+
findings.push({ level: "ok", message: "Commit workflow steps are valid" });
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if (steps.includes("test") && commitConfig.test?.command === "") {
|
|
1371
|
+
findings.push({
|
|
1372
|
+
level: "warn",
|
|
1373
|
+
message: "Commit test step has an empty command",
|
|
1374
|
+
fix: "Set commit.test.command or unset it to use the default pnpm test.",
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
if (steps.includes("build") && commitConfig.build?.command === "") {
|
|
1378
|
+
findings.push({
|
|
1379
|
+
level: "warn",
|
|
1380
|
+
message: "Commit build step has an empty command",
|
|
1381
|
+
fix: "Set commit.build.command or unset it to use the default pnpm build.",
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
async function validateReleaseConfig(
|
|
1387
|
+
releaseConfig: Record<string, any>,
|
|
1388
|
+
findings: IDoctorFinding[],
|
|
1389
|
+
): Promise<void> {
|
|
1390
|
+
const confirmation = releaseConfig.confirmation;
|
|
1391
|
+
if (confirmation === undefined || validConfirmationModes.includes(confirmation)) {
|
|
1392
|
+
findings.push({ level: "ok", message: "Release confirmation mode is valid" });
|
|
1393
|
+
} else {
|
|
1394
|
+
findings.push({
|
|
1395
|
+
level: "warn",
|
|
1396
|
+
message: `Invalid release confirmation mode: ${formatValue(confirmation)}`,
|
|
1397
|
+
fix: "Use prompt, auto, or plan.",
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (releaseConfig.registries || releaseConfig.accessLevel || releaseConfig.steps || releaseConfig.changelog) {
|
|
1402
|
+
findings.push({
|
|
1403
|
+
level: "warn",
|
|
1404
|
+
message: "Legacy release keys are present outside release.targets",
|
|
1405
|
+
fix: "Run `gitzone config migrate`.",
|
|
1406
|
+
});
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
const preflight = releaseConfig.preflight || {};
|
|
1410
|
+
if (preflight.test === true && preflight.testCommand === "") {
|
|
1411
|
+
findings.push({
|
|
1412
|
+
level: "warn",
|
|
1413
|
+
message: "Release test preflight has an empty command",
|
|
1414
|
+
fix: "Set release.preflight.testCommand or unset it to use pnpm test.",
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
if (preflight.build === true && preflight.buildCommand === "") {
|
|
1418
|
+
findings.push({
|
|
1419
|
+
level: "warn",
|
|
1420
|
+
message: "Release build preflight has an empty command",
|
|
1421
|
+
fix: "Set release.preflight.buildCommand or unset it to use pnpm build.",
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const targets = releaseConfig.targets || {};
|
|
1426
|
+
await validateGitTarget(targets.git || {}, findings);
|
|
1427
|
+
await validateNpmTarget(targets.npm || {}, findings);
|
|
1428
|
+
validateDockerTarget(targets.docker || {}, findings);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
async function validateGitTarget(
|
|
1432
|
+
gitTarget: Record<string, any>,
|
|
1433
|
+
findings: IDoctorFinding[],
|
|
1434
|
+
): Promise<void> {
|
|
1435
|
+
const enabled = gitTarget.enabled ?? true;
|
|
1436
|
+
if (!enabled) {
|
|
1437
|
+
findings.push({ level: "ok", message: "Git release target is disabled" });
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (gitTarget.remote === "") {
|
|
1442
|
+
findings.push({
|
|
1443
|
+
level: "error",
|
|
1444
|
+
message: "Git release target remote is empty",
|
|
1445
|
+
fix: "Set release.targets.git.remote or unset it to use origin.",
|
|
1446
|
+
});
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
findings.push({
|
|
1450
|
+
level: "ok",
|
|
1451
|
+
message: `Git release target is enabled (${gitTarget.remote || "origin"})`,
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
async function validateNpmTarget(
|
|
1456
|
+
npmTarget: Record<string, any>,
|
|
1457
|
+
findings: IDoctorFinding[],
|
|
1458
|
+
): Promise<void> {
|
|
1459
|
+
const registries = Array.isArray(npmTarget.registries) ? npmTarget.registries : [];
|
|
1460
|
+
const enabled = npmTarget.enabled ?? registries.length > 0;
|
|
1461
|
+
if (!enabled) {
|
|
1462
|
+
findings.push({ level: "ok", message: "npm release target is disabled" });
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
if (registries.length === 0) {
|
|
1467
|
+
findings.push({
|
|
1468
|
+
level: "error",
|
|
1469
|
+
message: "npm release target is enabled without registries",
|
|
1470
|
+
fix: "Run `gitzone config add https://registry.npmjs.org` or disable release.targets.npm.enabled.",
|
|
1471
|
+
});
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
if (npmTarget.accessLevel && !["public", "private"].includes(npmTarget.accessLevel)) {
|
|
1476
|
+
findings.push({
|
|
1477
|
+
level: "error",
|
|
1478
|
+
message: `Invalid npm access level: ${npmTarget.accessLevel}`,
|
|
1479
|
+
fix: "Use public or private.",
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
if (npmTarget.alreadyPublished && !["success", "error"].includes(npmTarget.alreadyPublished)) {
|
|
1483
|
+
findings.push({
|
|
1484
|
+
level: "error",
|
|
1485
|
+
message: `Invalid npm alreadyPublished behavior: ${npmTarget.alreadyPublished}`,
|
|
1486
|
+
fix: "Use success or error.",
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
const smartNetwork = new plugins.smartnetwork.SmartNetwork();
|
|
1491
|
+
await Promise.all(
|
|
1492
|
+
registries.map(async (registry) => {
|
|
1493
|
+
await validateNpmRegistry(registry, smartNetwork, findings);
|
|
1494
|
+
await validateNpmAuth(registry, findings);
|
|
1495
|
+
}),
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
async function validateNpmRegistry(
|
|
1500
|
+
registry: string,
|
|
1501
|
+
smartNetwork: plugins.smartnetwork.SmartNetwork,
|
|
1502
|
+
findings: IDoctorFinding[],
|
|
1503
|
+
): Promise<void> {
|
|
1504
|
+
const normalizedRegistry = normalizeRegistryUrl(registry);
|
|
1505
|
+
if (normalizedRegistry !== registry) {
|
|
1506
|
+
findings.push({
|
|
1507
|
+
level: "warn",
|
|
1508
|
+
message: `npm registry should be normalized: ${registry}`,
|
|
1509
|
+
fix: `Use ${normalizedRegistry}`,
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
let registryUrl: URL;
|
|
1514
|
+
try {
|
|
1515
|
+
registryUrl = new URL(normalizedRegistry);
|
|
1516
|
+
} catch {
|
|
1517
|
+
findings.push({
|
|
1518
|
+
level: "error",
|
|
1519
|
+
message: `Invalid npm registry URL: ${registry}`,
|
|
1520
|
+
fix: "Use a valid http or https registry URL.",
|
|
1521
|
+
});
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
if (registryUrl.protocol !== "https:" && registryUrl.protocol !== "http:") {
|
|
1526
|
+
findings.push({
|
|
1527
|
+
level: "error",
|
|
1528
|
+
message: `Unsupported npm registry protocol: ${registryUrl.protocol}`,
|
|
1529
|
+
fix: "Use an http or https registry URL.",
|
|
1530
|
+
});
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
try {
|
|
1535
|
+
const result = await smartNetwork.checkEndpoint(normalizedRegistry, { timeout: 5000 });
|
|
1536
|
+
if (result.status >= 200 && result.status < 500) {
|
|
1537
|
+
findings.push({
|
|
1538
|
+
level: "ok",
|
|
1539
|
+
message: `npm registry is reachable: ${normalizedRegistry} (${result.status})`,
|
|
1540
|
+
});
|
|
1541
|
+
} else {
|
|
1542
|
+
findings.push({
|
|
1543
|
+
level: "warn",
|
|
1544
|
+
message: `npm registry returned status ${result.status}: ${normalizedRegistry}`,
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
} catch (error) {
|
|
1548
|
+
findings.push({
|
|
1549
|
+
level: "warn",
|
|
1550
|
+
message: `npm registry is not reachable: ${normalizedRegistry}`,
|
|
1551
|
+
fix: error instanceof Error ? error.message : String(error),
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
async function validateNpmAuth(
|
|
1557
|
+
registry: string,
|
|
1558
|
+
findings: IDoctorFinding[],
|
|
1559
|
+
): Promise<void> {
|
|
1560
|
+
const normalizedRegistry = normalizeRegistryUrl(registry);
|
|
1561
|
+
const smartshellInstance = new plugins.smartshell.Smartshell({
|
|
1562
|
+
executor: "bash",
|
|
1563
|
+
sourceFilePaths: [],
|
|
1564
|
+
});
|
|
1565
|
+
try {
|
|
1566
|
+
const result = await smartshellInstance.execSpawn(
|
|
1567
|
+
"pnpm",
|
|
1568
|
+
["npm", "whoami", `--registry=${normalizedRegistry}`],
|
|
1569
|
+
{ silent: true, timeout: 8000 },
|
|
1570
|
+
);
|
|
1571
|
+
if (result.exitCode === 0) {
|
|
1572
|
+
findings.push({
|
|
1573
|
+
level: "ok",
|
|
1574
|
+
message: `npm auth is available for ${normalizedRegistry}`,
|
|
1575
|
+
});
|
|
1576
|
+
} else {
|
|
1577
|
+
findings.push({
|
|
1578
|
+
level: "warn",
|
|
1579
|
+
message: `npm auth is missing or invalid for ${normalizedRegistry}`,
|
|
1580
|
+
fix: `Run pnpm npm login --registry=${normalizedRegistry}`,
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
findings.push({
|
|
1585
|
+
level: "warn",
|
|
1586
|
+
message: `Could not check npm auth for ${normalizedRegistry}`,
|
|
1587
|
+
fix: error instanceof Error ? error.message : String(error),
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function validateDockerTarget(
|
|
1593
|
+
dockerTarget: Record<string, any>,
|
|
1594
|
+
findings: IDoctorFinding[],
|
|
1595
|
+
): void {
|
|
1596
|
+
const enabled = dockerTarget.enabled ?? false;
|
|
1597
|
+
if (!enabled) {
|
|
1598
|
+
findings.push({ level: "ok", message: "Docker release target is disabled" });
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
if (!Array.isArray(dockerTarget.images) || dockerTarget.images.length === 0) {
|
|
1603
|
+
findings.push({
|
|
1604
|
+
level: "error",
|
|
1605
|
+
message: "Docker release target is enabled without images",
|
|
1606
|
+
fix: "Set release.targets.docker.images or disable release.targets.docker.enabled.",
|
|
1607
|
+
});
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
findings.push({
|
|
1612
|
+
level: "ok",
|
|
1613
|
+
message: `Docker release target has ${dockerTarget.images.length} image template(s)`,
|
|
1614
|
+
});
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
async function validateDetectedProjectType(
|
|
1618
|
+
cliConfig: Record<string, any>,
|
|
1619
|
+
findings: IDoctorFinding[],
|
|
1620
|
+
): Promise<void> {
|
|
1621
|
+
const packageJsonPath = plugins.path.join(process.cwd(), "package.json");
|
|
1622
|
+
const denoJsonPath = plugins.path.join(process.cwd(), "deno.json");
|
|
1623
|
+
const hasPackageJson = await plugins.smartfs.file(packageJsonPath).exists();
|
|
1624
|
+
const hasDenoJson = await plugins.smartfs.file(denoJsonPath).exists();
|
|
1625
|
+
|
|
1626
|
+
if (!hasPackageJson && !hasDenoJson) {
|
|
1627
|
+
findings.push({
|
|
1628
|
+
level: "warn",
|
|
1629
|
+
message: "Could not detect package.json or deno.json",
|
|
1630
|
+
fix: "Run this command from a project root.",
|
|
1631
|
+
});
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
if (hasPackageJson && validProjectTypes.includes(cliConfig.projectType)) {
|
|
1636
|
+
findings.push({
|
|
1637
|
+
level: "ok",
|
|
1638
|
+
message: "Detected project files match configured npm-compatible project type",
|
|
1639
|
+
});
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
if (hasDenoJson && !hasPackageJson) {
|
|
1644
|
+
findings.push({
|
|
1645
|
+
level: "warn",
|
|
1646
|
+
message: "Detected a Deno-only project, but guided config supports npm-compatible project types",
|
|
1647
|
+
fix: "Use `gitzone config set projectType npm|service|wcc|website` only for npm-compatible projects.",
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
|
|
718
1652
|
/**
|
|
719
1653
|
* Show help for config command
|
|
720
1654
|
*/
|
|
@@ -728,6 +1662,10 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
728
1662
|
name: "show",
|
|
729
1663
|
description: "Display current @git.zone/cli configuration",
|
|
730
1664
|
},
|
|
1665
|
+
{ name: "project", description: "Configure project basics interactively" },
|
|
1666
|
+
{ name: "cli", description: "Configure CLI behavior interactively" },
|
|
1667
|
+
{ name: "release", description: "Configure release workflow interactively" },
|
|
1668
|
+
{ name: "doctor", description: "Validate .smartconfig.json" },
|
|
731
1669
|
{ name: "get <path>", description: "Read a single config value" },
|
|
732
1670
|
{ name: "set <path> <value>", description: "Write a config value" },
|
|
733
1671
|
{ name: "unset <path>", description: "Delete a config value" },
|
|
@@ -749,6 +1687,8 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
749
1687
|
],
|
|
750
1688
|
examples: [
|
|
751
1689
|
"gitzone config show --json",
|
|
1690
|
+
"gitzone config project",
|
|
1691
|
+
"gitzone config doctor --json",
|
|
752
1692
|
"gitzone config get release.targets.npm.accessLevel",
|
|
753
1693
|
"gitzone config set cli.interactive false",
|
|
754
1694
|
"gitzone config set cli.output json",
|
|
@@ -764,6 +1704,10 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
764
1704
|
console.log(
|
|
765
1705
|
" show Display current @git.zone/cli configuration",
|
|
766
1706
|
);
|
|
1707
|
+
console.log(" project Configure project basics interactively");
|
|
1708
|
+
console.log(" cli Configure CLI behavior interactively");
|
|
1709
|
+
console.log(" release Configure release workflow interactively");
|
|
1710
|
+
console.log(" doctor Validate .smartconfig.json");
|
|
767
1711
|
console.log(" get <path> Read a single config value");
|
|
768
1712
|
console.log(" set <path> <value> Write a config value");
|
|
769
1713
|
console.log(" unset <path> Delete a config value");
|
|
@@ -782,6 +1726,10 @@ export function showHelp(mode?: ICliMode): void {
|
|
|
782
1726
|
console.log("Examples:");
|
|
783
1727
|
console.log(" gitzone config show");
|
|
784
1728
|
console.log(" gitzone config show --json");
|
|
1729
|
+
console.log(" gitzone config project");
|
|
1730
|
+
console.log(" gitzone config cli");
|
|
1731
|
+
console.log(" gitzone config release");
|
|
1732
|
+
console.log(" gitzone config doctor --json");
|
|
785
1733
|
console.log(" gitzone config get release.targets.npm.accessLevel");
|
|
786
1734
|
console.log(" gitzone config set cli.interactive false");
|
|
787
1735
|
console.log(" gitzone config set cli.output json");
|