@git.zone/cli 2.16.1 → 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.
@@ -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 registry configuration
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
- const smartconfigData = await readSmartconfigFile();
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
- "│ Release NPM Target Configuration │",
239
+ "│ gitzone config - Project Configuration │",
212
240
  );
213
241
  console.log(
214
242
  "╰─────────────────────────────────────────────────────────────╯",
215
243
  );
216
244
  console.log("");
217
245
 
218
- // Show access level
219
- plugins.logger.log("info", `Access Level: ${accessLevel}`);
220
- console.log("");
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
- message: "Enter npm target registry URL:",
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");