@git.zone/cli 2.16.1 → 2.18.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,24 @@ 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;
120
+ case "fix":
121
+ await handleFix(argvArg, mode);
122
+ break;
106
123
  case "migrate":
107
124
  await handleMigrate(value, mode);
108
125
  break;
@@ -145,13 +162,18 @@ async function handleInteractiveMenu(): Promise<void> {
145
162
  default: "show",
146
163
  choices: [
147
164
  { name: "Show current configuration", value: "show" },
165
+ { name: "Configure project basics", value: "project" },
166
+ { name: "Configure CLI behavior", value: "cli" },
167
+ { name: "Configure commit workflow", value: "commit" },
168
+ { name: "Configure release workflow", value: "release" },
169
+ { name: "Configure services", value: "services" },
170
+ { name: "Validate configuration (doctor)", value: "doctor" },
171
+ { name: "Fix configuration with opencode", value: "fix" },
148
172
  { name: "Add an npm target registry", value: "add" },
149
173
  { name: "Remove an npm target registry", value: "remove" },
150
174
  { name: "Clear npm target registries", value: "clear" },
151
175
  { name: "Set access level (public/private)", value: "access" },
152
176
  { name: "Migrate smartconfig schema", value: "migrate" },
153
- { name: "Configure commit options", value: "commit" },
154
- { name: "Configure services", value: "services" },
155
177
  { name: "Show help", value: "help" },
156
178
  ],
157
179
  });
@@ -162,6 +184,15 @@ async function handleInteractiveMenu(): Promise<void> {
162
184
  case "show":
163
185
  await handleShow(defaultCliMode);
164
186
  break;
187
+ case "project":
188
+ await handleProject(defaultCliMode);
189
+ break;
190
+ case "cli":
191
+ await handleCli(defaultCliMode);
192
+ break;
193
+ case "release":
194
+ await handleRelease(defaultCliMode);
195
+ break;
165
196
  case "add":
166
197
  await handleAdd(undefined, defaultCliMode);
167
198
  break;
@@ -183,6 +214,12 @@ async function handleInteractiveMenu(): Promise<void> {
183
214
  case "services":
184
215
  await handleServices(defaultCliMode);
185
216
  break;
217
+ case "doctor":
218
+ await handleDoctor(defaultCliMode);
219
+ break;
220
+ case "fix":
221
+ await handleFix({ _: ["config", "fix"] }, defaultCliMode);
222
+ break;
186
223
  case "help":
187
224
  showHelp();
188
225
  break;
@@ -190,48 +227,80 @@ async function handleInteractiveMenu(): Promise<void> {
190
227
  }
191
228
 
192
229
  /**
193
- * Show current registry configuration
230
+ * Show current CLI project configuration
194
231
  */
195
232
  async function handleShow(mode: ICliMode): Promise<void> {
233
+ const smartconfigData = await readSmartconfigFile();
234
+ const cliConfig = getCliConfigValueFromData(smartconfigData, "") || {};
235
+
196
236
  if (mode.json) {
197
- const smartconfigData = await readSmartconfigFile();
198
- printJson(getCliConfigValueFromData(smartconfigData, ""));
237
+ printJson(cliConfig);
199
238
  return;
200
239
  }
201
240
 
202
- const config = await ReleaseConfig.fromCwd();
203
- const registries = config.getRegistries();
204
- const accessLevel = config.getAccessLevel();
205
-
206
241
  console.log("");
207
242
  console.log(
208
243
  "╭─────────────────────────────────────────────────────────────╮",
209
244
  );
210
245
  console.log(
211
- "│ Release NPM Target Configuration │",
246
+ "│ gitzone config - Project Configuration │",
212
247
  );
213
248
  console.log(
214
249
  "╰─────────────────────────────────────────────────────────────╯",
215
250
  );
216
251
  console.log("");
217
252
 
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
- });
253
+ if (Object.keys(cliConfig).length === 0) {
254
+ plugins.logger.log("warn", `No ${CLI_NAMESPACE} configuration found.`);
255
+ console.log(" Run `gitzone config project` to create project basics.");
233
256
  console.log("");
257
+ return;
234
258
  }
259
+
260
+ printConfigSection("Project", [
261
+ ["schemaVersion", formatValue(cliConfig.schemaVersion)],
262
+ ["projectType", formatValue(cliConfig.projectType)],
263
+ ["repository", formatRepository(cliConfig.module)],
264
+ ["description", formatValue(cliConfig.module?.description)],
265
+ ["npm package", formatValue(cliConfig.module?.npmPackagename || cliConfig.module?.npmPackageName)],
266
+ ["license", formatValue(cliConfig.module?.license)],
267
+ ["keywords", formatList(cliConfig.module?.keywords)],
268
+ ]);
269
+
270
+ printConfigSection("CLI Behavior", [
271
+ ["interactive", formatValue(cliConfig.cli?.interactive)],
272
+ ["output", formatValue(cliConfig.cli?.output)],
273
+ ["checkUpdates", formatValue(cliConfig.cli?.checkUpdates)],
274
+ ]);
275
+
276
+ printConfigSection("Commit Workflow", [
277
+ ["confirmation", formatValue(cliConfig.commit?.confirmation)],
278
+ ["steps", formatList(cliConfig.commit?.steps)],
279
+ ["test command", formatValue(cliConfig.commit?.test?.command)],
280
+ ["build command", formatValue(cliConfig.commit?.build?.command)],
281
+ ["push remote", formatValue(cliConfig.commit?.push?.remote)],
282
+ ["push followTags", formatValue(cliConfig.commit?.push?.followTags)],
283
+ ]);
284
+
285
+ const release = cliConfig.release || {};
286
+ const targets = release.targets || {};
287
+ printConfigSection("Release Workflow", [
288
+ ["confirmation", formatValue(release.confirmation)],
289
+ ["require clean tree", formatValue(release.preflight?.requireCleanTree)],
290
+ ["run tests", formatValue(release.preflight?.test)],
291
+ ["run build", formatValue(release.preflight?.build)],
292
+ ["test command", formatValue(release.preflight?.testCommand)],
293
+ ["build command", formatValue(release.preflight?.buildCommand)],
294
+ ]);
295
+
296
+ printConfigSection("Release Targets", [
297
+ ["git", formatTarget(targets.git?.enabled, targets.git)],
298
+ ["npm", formatTarget(targets.npm?.enabled, targets.npm)],
299
+ ["docker", formatTarget(targets.docker?.enabled, targets.docker)],
300
+ ]);
301
+
302
+ console.log("Run `gitzone config doctor` to validate this configuration.");
303
+ console.log("");
235
304
  }
236
305
 
237
306
  /**
@@ -251,7 +320,7 @@ async function handleAdd(
251
320
  const response = await interactInstance.askQuestion({
252
321
  type: "input",
253
322
  name: "registryUrl",
254
- message: "Enter npm target registry URL:",
323
+ message: "Enter npm target registry URL:",
255
324
  default: "https://registry.npmjs.org",
256
325
  validate: (input: string) => {
257
326
  return !!(input && input.trim() !== "");
@@ -547,6 +616,438 @@ function showCommitHelp(): void {
547
616
  console.log("");
548
617
  }
549
618
 
619
+ async function handleProject(mode: ICliMode): Promise<void> {
620
+ if (!mode.interactive) {
621
+ throw new Error("Project configuration requires interactive mode. Use `gitzone config set` for automation.");
622
+ }
623
+
624
+ const smartconfigData = await readSmartconfigFile();
625
+ const cliConfig = getCliConfigValueFromData(smartconfigData, "") || {};
626
+ const moduleConfig = cliConfig.module || {};
627
+ const packageJson = await readPackageJson();
628
+ const interactInstance = new plugins.smartinteract.SmartInteract();
629
+
630
+ const projectType = await askValue<string>(interactInstance, {
631
+ type: "list",
632
+ name: "projectType",
633
+ message: "What kind of project is this?",
634
+ choices: ["npm", "service", "wcc", "website"],
635
+ default: cliConfig.projectType || "npm",
636
+ });
637
+ const githost = await askValue<string>(interactInstance, {
638
+ type: "input",
639
+ name: "githost",
640
+ message: "Git host:",
641
+ default: moduleConfig.githost || "code.foss.global",
642
+ });
643
+ const gitscope = await askValue<string>(interactInstance, {
644
+ type: "input",
645
+ name: "gitscope",
646
+ message: "Git scope/owner:",
647
+ default: moduleConfig.gitscope || "git.zone",
648
+ });
649
+ const gitrepo = await askValue<string>(interactInstance, {
650
+ type: "input",
651
+ name: "gitrepo",
652
+ message: "Git repository name:",
653
+ default: moduleConfig.gitrepo || inferRepoName(packageJson.name),
654
+ });
655
+ const description = await askValue<string>(interactInstance, {
656
+ type: "input",
657
+ name: "description",
658
+ message: "Project description:",
659
+ default: moduleConfig.description || packageJson.description || "",
660
+ });
661
+ const npmPackagename = await askValue<string>(interactInstance, {
662
+ type: "input",
663
+ name: "npmPackagename",
664
+ message: "npm package name:",
665
+ default: moduleConfig.npmPackagename || moduleConfig.npmPackageName || packageJson.name || "",
666
+ });
667
+ const license = await askValue<string>(interactInstance, {
668
+ type: "input",
669
+ name: "license",
670
+ message: "License:",
671
+ default: moduleConfig.license || packageJson.license || "MIT",
672
+ });
673
+ const keywords = await askValue<string>(interactInstance, {
674
+ type: "input",
675
+ name: "keywords",
676
+ message: "Keywords (comma-separated):",
677
+ default: Array.isArray(moduleConfig.keywords)
678
+ ? moduleConfig.keywords.join(", ")
679
+ : Array.isArray(packageJson.keywords)
680
+ ? packageJson.keywords.join(", ")
681
+ : "",
682
+ });
683
+
684
+ setCliConfigValueInData(smartconfigData, "schemaVersion", CURRENT_GITZONE_CLI_SCHEMA_VERSION);
685
+ setCliConfigValueInData(smartconfigData, "projectType", projectType);
686
+ setCliConfigValueInData(smartconfigData, "module", {
687
+ ...moduleConfig,
688
+ githost: githost.trim(),
689
+ gitscope: gitscope.trim(),
690
+ gitrepo: gitrepo.trim(),
691
+ description: description.trim(),
692
+ npmPackagename: npmPackagename.trim(),
693
+ license: license.trim(),
694
+ keywords: parseCsv(keywords),
695
+ });
696
+ await writeSmartconfigFile(smartconfigData);
697
+ plugins.logger.log("success", "Project configuration updated");
698
+ await formatSmartconfigWithDiff(mode);
699
+ }
700
+
701
+ async function handleCli(mode: ICliMode): Promise<void> {
702
+ if (!mode.interactive) {
703
+ throw new Error("CLI behavior configuration requires interactive mode. Use `gitzone config set` for automation.");
704
+ }
705
+
706
+ const smartconfigData = await readSmartconfigFile();
707
+ const cliConfig = getCliConfigValueFromData(smartconfigData, "cli") || {};
708
+ const interactInstance = new plugins.smartinteract.SmartInteract();
709
+
710
+ const output = await askValue<string>(interactInstance, {
711
+ type: "list",
712
+ name: "output",
713
+ message: "Default output mode:",
714
+ choices: ["human", "plain", "json"],
715
+ default: cliConfig.output || "human",
716
+ });
717
+ const interactive = await askValue<boolean>(interactInstance, {
718
+ type: "confirm",
719
+ name: "interactive",
720
+ message: "Enable interactive prompts by default?",
721
+ default: cliConfig.interactive ?? true,
722
+ });
723
+ const checkUpdates = await askValue<boolean>(interactInstance, {
724
+ type: "confirm",
725
+ name: "checkUpdates",
726
+ message: "Check for gitzone updates in human mode?",
727
+ default: cliConfig.checkUpdates ?? true,
728
+ });
729
+
730
+ setCliConfigValueInData(smartconfigData, "schemaVersion", CURRENT_GITZONE_CLI_SCHEMA_VERSION);
731
+ setCliConfigValueInData(smartconfigData, "cli", {
732
+ output,
733
+ interactive,
734
+ checkUpdates,
735
+ });
736
+ await writeSmartconfigFile(smartconfigData);
737
+ plugins.logger.log("success", "CLI behavior configuration updated");
738
+ await formatSmartconfigWithDiff(mode);
739
+ }
740
+
741
+ async function handleRelease(mode: ICliMode): Promise<void> {
742
+ if (!mode.interactive) {
743
+ throw new Error("Release configuration requires interactive mode. Use `gitzone config set` for automation.");
744
+ }
745
+
746
+ const smartconfigData = await readSmartconfigFile();
747
+ const currentRelease = getCliConfigValueFromData(smartconfigData, "release") || {};
748
+ const currentTargets = currentRelease.targets || {};
749
+ const interactInstance = new plugins.smartinteract.SmartInteract();
750
+
751
+ const confirmation = await askValue<string>(interactInstance, {
752
+ type: "list",
753
+ name: "confirmation",
754
+ message: "Release confirmation mode:",
755
+ choices: ["prompt", "auto", "plan"],
756
+ default: currentRelease.confirmation || "prompt",
757
+ });
758
+ const requireCleanTree = await askValue<boolean>(interactInstance, {
759
+ type: "confirm",
760
+ name: "requireCleanTree",
761
+ message: "Require a clean git tree before release?",
762
+ default: currentRelease.preflight?.requireCleanTree ?? true,
763
+ });
764
+ const runTests = await askValue<boolean>(interactInstance, {
765
+ type: "confirm",
766
+ name: "runTests",
767
+ message: "Run tests during release preflight?",
768
+ default: currentRelease.preflight?.test ?? false,
769
+ });
770
+ const runBuild = await askValue<boolean>(interactInstance, {
771
+ type: "confirm",
772
+ name: "runBuild",
773
+ message: "Run build during release?",
774
+ default: currentRelease.preflight?.build ?? true,
775
+ });
776
+ const testCommand = await askValue<string>(interactInstance, {
777
+ type: "input",
778
+ name: "testCommand",
779
+ message: "Release test command:",
780
+ default: currentRelease.preflight?.testCommand || "pnpm test",
781
+ });
782
+ const buildCommand = await askValue<string>(interactInstance, {
783
+ type: "input",
784
+ name: "buildCommand",
785
+ message: "Release build command:",
786
+ default: currentRelease.preflight?.buildCommand || "pnpm build",
787
+ });
788
+
789
+ const enabledTargets = await askValue<string[]>(interactInstance, {
790
+ type: "checkbox",
791
+ name: "targets",
792
+ message: "Enable release targets:",
793
+ choices: [
794
+ { name: "git - push branch and tags", value: "git" },
795
+ { name: "npm - publish package registries", value: "npm" },
796
+ { name: "docker - build and push images", value: "docker" },
797
+ ],
798
+ default: getDefaultEnabledTargets(currentTargets),
799
+ });
800
+
801
+ const releaseTargets: Record<string, any> = { ...currentTargets };
802
+
803
+ if (enabledTargets.includes("git")) {
804
+ releaseTargets.git = {
805
+ ...(currentTargets.git || {}),
806
+ enabled: true,
807
+ remote: await askValue<string>(interactInstance, {
808
+ type: "input",
809
+ name: "gitRemote",
810
+ message: "Git remote:",
811
+ default: currentTargets.git?.remote || "origin",
812
+ }),
813
+ pushBranch: await askValue<boolean>(interactInstance, {
814
+ type: "confirm",
815
+ name: "pushBranch",
816
+ message: "Push release commit branch?",
817
+ default: currentTargets.git?.pushBranch ?? true,
818
+ }),
819
+ pushTags: await askValue<boolean>(interactInstance, {
820
+ type: "confirm",
821
+ name: "pushTags",
822
+ message: "Push release tags?",
823
+ default: currentTargets.git?.pushTags ?? true,
824
+ }),
825
+ };
826
+ } else {
827
+ releaseTargets.git = { ...(currentTargets.git || {}), enabled: false };
828
+ }
829
+
830
+ if (enabledTargets.includes("npm")) {
831
+ const registries = await askValue<string>(interactInstance, {
832
+ type: "input",
833
+ name: "npmRegistries",
834
+ message: "npm registries (comma-separated):",
835
+ default: Array.isArray(currentTargets.npm?.registries)
836
+ ? currentTargets.npm.registries.join(", ")
837
+ : "https://registry.npmjs.org",
838
+ });
839
+ releaseTargets.npm = {
840
+ ...(currentTargets.npm || {}),
841
+ enabled: true,
842
+ registries: parseCsv(registries).map(normalizeRegistryUrl),
843
+ accessLevel: await askValue<string>(interactInstance, {
844
+ type: "list",
845
+ name: "npmAccessLevel",
846
+ message: "npm publish access level:",
847
+ choices: ["public", "private"],
848
+ default: currentTargets.npm?.accessLevel || "public",
849
+ }),
850
+ alreadyPublished: await askValue<string>(interactInstance, {
851
+ type: "list",
852
+ name: "alreadyPublished",
853
+ message: "When a package version is already published:",
854
+ choices: ["success", "error"],
855
+ default: currentTargets.npm?.alreadyPublished || "success",
856
+ }),
857
+ };
858
+ } else {
859
+ releaseTargets.npm = { ...(currentTargets.npm || {}), enabled: false };
860
+ }
861
+
862
+ if (enabledTargets.includes("docker")) {
863
+ const images = await askValue<string>(interactInstance, {
864
+ type: "input",
865
+ name: "dockerImages",
866
+ message: "Docker image templates (comma-separated, supports {{version}}):",
867
+ default: Array.isArray(currentTargets.docker?.images)
868
+ ? currentTargets.docker.images.join(", ")
869
+ : "",
870
+ });
871
+ releaseTargets.docker = {
872
+ ...(currentTargets.docker || {}),
873
+ enabled: true,
874
+ images: parseCsv(images),
875
+ };
876
+ } else {
877
+ releaseTargets.docker = { ...(currentTargets.docker || {}), enabled: false };
878
+ }
879
+
880
+ setCliConfigValueInData(smartconfigData, "schemaVersion", CURRENT_GITZONE_CLI_SCHEMA_VERSION);
881
+ setCliConfigValueInData(smartconfigData, "release", {
882
+ ...currentRelease,
883
+ confirmation,
884
+ preflight: {
885
+ ...(currentRelease.preflight || {}),
886
+ requireCleanTree,
887
+ test: runTests,
888
+ build: runBuild,
889
+ testCommand: testCommand.trim(),
890
+ buildCommand: buildCommand.trim(),
891
+ },
892
+ targets: releaseTargets,
893
+ });
894
+ await writeSmartconfigFile(smartconfigData);
895
+ plugins.logger.log("success", "Release configuration updated");
896
+ await formatSmartconfigWithDiff(mode);
897
+ }
898
+
899
+ async function handleDoctor(mode: ICliMode): Promise<void> {
900
+ const findings = await collectDoctorFindings();
901
+ printDoctorResult(findings, mode);
902
+ }
903
+
904
+ async function handleFix(argvArg: any, mode: ICliMode): Promise<void> {
905
+ if (mode.json) {
906
+ printJson({
907
+ ok: false,
908
+ error: "JSON output is not supported for `gitzone config fix`. Use `gitzone config doctor --json` for machine-readable diagnostics.",
909
+ });
910
+ process.exitCode = 1;
911
+ return;
912
+ }
913
+
914
+ const findings = await collectDoctorFindings();
915
+ const counts = countDoctorFindings(findings);
916
+ const extraInstructions = (argvArg._?.slice(2).join(" ") || "").trim();
917
+ const force = Boolean(argvArg.force);
918
+
919
+ if (counts.error === 0 && counts.warn === 0 && !extraInstructions && !force) {
920
+ plugins.logger.log(
921
+ "success",
922
+ "Configuration doctor found no issues. Use `gitzone config fix --force` to run opencode anyway.",
923
+ );
924
+ return;
925
+ }
926
+
927
+ if (!mode.yes) {
928
+ if (!mode.interactive) {
929
+ throw new Error("Config fix requires an interactive terminal or `-y` to run opencode non-interactively.");
930
+ }
931
+ const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
932
+ `Run opencode to fix .smartconfig.json? (${counts.error} error, ${counts.warn} warning)`,
933
+ true,
934
+ );
935
+ if (!confirmed) {
936
+ plugins.logger.log("info", "Config fix cancelled.");
937
+ return;
938
+ }
939
+ }
940
+
941
+ const opencodeArgs = [
942
+ "run",
943
+ "--title",
944
+ "gitzone config fix",
945
+ "--dir",
946
+ process.cwd(),
947
+ ];
948
+ if (mode.yes) {
949
+ opencodeArgs.push("--dangerously-skip-permissions");
950
+ }
951
+ opencodeArgs.push(buildConfigFixPrompt(findings, extraInstructions));
952
+
953
+ plugins.logger.log("info", "Starting opencode configuration fix...");
954
+ const smartshellInstance = new plugins.smartshell.Smartshell({
955
+ executor: "bash",
956
+ sourceFilePaths: [],
957
+ });
958
+
959
+ let result: plugins.smartshell.IExecResult;
960
+ try {
961
+ result = await smartshellInstance.execSpawn("opencode", opencodeArgs, {
962
+ passthrough: true,
963
+ });
964
+ } catch (error) {
965
+ throw new Error(`Failed to run opencode: ${error instanceof Error ? error.message : String(error)}`);
966
+ }
967
+
968
+ if (result.exitCode !== 0) {
969
+ plugins.logger.log("error", `opencode exited with code ${result.exitCode}`);
970
+ process.exitCode = result.exitCode || 1;
971
+ return;
972
+ }
973
+
974
+ await formatSmartconfigWithDiff(mode);
975
+ const finalFindings = await collectDoctorFindings();
976
+ printDoctorResult(finalFindings, mode);
977
+ }
978
+
979
+ async function collectDoctorFindings(): Promise<IDoctorFinding[]> {
980
+ const findings: IDoctorFinding[] = [];
981
+ const smartconfigPath = getSmartconfigPath();
982
+ const smartconfigExists = await plugins.smartfs.file(smartconfigPath).exists();
983
+
984
+ if (!smartconfigExists) {
985
+ findings.push({
986
+ level: "warn",
987
+ message: ".smartconfig.json does not exist",
988
+ fix: "Run `gitzone config project` to create project basics.",
989
+ });
990
+ return findings;
991
+ }
992
+
993
+ let smartconfigData: Record<string, any>;
994
+ try {
995
+ smartconfigData = await readSmartconfigFile();
996
+ } catch (error) {
997
+ findings.push({
998
+ level: "error",
999
+ message: ".smartconfig.json is not valid JSON",
1000
+ fix: error instanceof Error ? error.message : String(error),
1001
+ });
1002
+ return findings;
1003
+ }
1004
+
1005
+ const cliConfig = getCliConfigValueFromData(smartconfigData, "") || {};
1006
+ if (Object.keys(cliConfig).length === 0) {
1007
+ findings.push({
1008
+ level: "error",
1009
+ message: `${CLI_NAMESPACE} configuration is missing`,
1010
+ fix: "Run `gitzone config project` or `gitzone config migrate`.",
1011
+ });
1012
+ }
1013
+
1014
+ for (const legacyNamespace of ["gitzone", "tsdoc", "npmdocker", "npmci", "szci"]) {
1015
+ if (smartconfigData[legacyNamespace]) {
1016
+ findings.push({
1017
+ level: "warn",
1018
+ message: `Legacy namespace '${legacyNamespace}' is present`,
1019
+ fix: "Run `gitzone config migrate`.",
1020
+ });
1021
+ }
1022
+ }
1023
+
1024
+ if (cliConfig.schemaVersion === CURRENT_GITZONE_CLI_SCHEMA_VERSION) {
1025
+ findings.push({ level: "ok", message: `Schema version is current (${CURRENT_GITZONE_CLI_SCHEMA_VERSION})` });
1026
+ } else {
1027
+ findings.push({
1028
+ level: "warn",
1029
+ message: `Schema version is ${formatValue(cliConfig.schemaVersion)}, expected ${CURRENT_GITZONE_CLI_SCHEMA_VERSION}`,
1030
+ fix: "Run `gitzone config migrate`.",
1031
+ });
1032
+ }
1033
+
1034
+ if (["npm", "service", "wcc", "website"].includes(cliConfig.projectType)) {
1035
+ findings.push({ level: "ok", message: `Project type is ${cliConfig.projectType}` });
1036
+ } else {
1037
+ findings.push({
1038
+ level: "warn",
1039
+ message: `Project type is missing or invalid: ${formatValue(cliConfig.projectType)}`,
1040
+ fix: "Run `gitzone config project`.",
1041
+ });
1042
+ }
1043
+ await validateDetectedProjectType(cliConfig, findings);
1044
+
1045
+ validateCommitConfig(cliConfig.commit || {}, findings);
1046
+ await validateReleaseConfig(cliConfig.release || {}, findings);
1047
+
1048
+ return findings;
1049
+ }
1050
+
550
1051
  /**
551
1052
  * Handle services configuration
552
1053
  */
@@ -715,6 +1216,568 @@ function parseConfigValue(rawValue: string): any {
715
1216
  return rawValue;
716
1217
  }
717
1218
 
1219
+ type TDoctorFindingLevel = "ok" | "warn" | "error";
1220
+
1221
+ interface IDoctorFinding {
1222
+ level: TDoctorFindingLevel;
1223
+ message: string;
1224
+ fix?: string;
1225
+ }
1226
+
1227
+ const validProjectTypes = ["npm", "service", "wcc", "website"];
1228
+ const validConfirmationModes = ["prompt", "auto", "plan"];
1229
+ const validCommitSteps = [
1230
+ "format",
1231
+ "analyze",
1232
+ "test",
1233
+ "build",
1234
+ "changelog",
1235
+ "commit",
1236
+ "push",
1237
+ ];
1238
+
1239
+ function printConfigSection(
1240
+ title: string,
1241
+ rows: Array<[string, string]>,
1242
+ ): void {
1243
+ console.log(`${title}:`);
1244
+ for (const [label, value] of rows) {
1245
+ console.log(` ${label.padEnd(20)} ${value}`);
1246
+ }
1247
+ console.log("");
1248
+ }
1249
+
1250
+ function formatValue(value: unknown): string {
1251
+ if (value === undefined || value === null || value === "") {
1252
+ return "(unset)";
1253
+ }
1254
+ if (typeof value === "boolean") {
1255
+ return value ? "true" : "false";
1256
+ }
1257
+ if (typeof value === "string" || typeof value === "number") {
1258
+ return String(value);
1259
+ }
1260
+ return JSON.stringify(value);
1261
+ }
1262
+
1263
+ function formatRepository(moduleConfig: any): string {
1264
+ if (!moduleConfig) {
1265
+ return "(unset)";
1266
+ }
1267
+ const parts = [
1268
+ moduleConfig.githost,
1269
+ moduleConfig.gitscope,
1270
+ moduleConfig.gitrepo,
1271
+ ].filter(Boolean);
1272
+ return parts.length > 0 ? parts.join("/") : "(unset)";
1273
+ }
1274
+
1275
+ function formatList(value: unknown): string {
1276
+ if (!Array.isArray(value) || value.length === 0) {
1277
+ return "(none)";
1278
+ }
1279
+ return value.map((item) => String(item)).join(", ");
1280
+ }
1281
+
1282
+ function formatTarget(enabled: unknown, targetConfig: any): string {
1283
+ const state = enabled === false ? "disabled" : "enabled";
1284
+ if (!targetConfig || Object.keys(targetConfig).length === 0) {
1285
+ return state;
1286
+ }
1287
+
1288
+ const details: string[] = [];
1289
+ if (targetConfig.remote) details.push(`remote=${targetConfig.remote}`);
1290
+ if (Array.isArray(targetConfig.registries)) {
1291
+ details.push(`registries=${targetConfig.registries.length}`);
1292
+ }
1293
+ if (targetConfig.accessLevel) details.push(`access=${targetConfig.accessLevel}`);
1294
+ if (Array.isArray(targetConfig.images)) {
1295
+ details.push(`images=${targetConfig.images.length}`);
1296
+ }
1297
+ return details.length > 0 ? `${state} (${details.join(", ")})` : state;
1298
+ }
1299
+
1300
+ async function askValue<T>(
1301
+ interactInstance: any,
1302
+ options: any,
1303
+ ): Promise<T> {
1304
+ const response = await interactInstance.askQuestion(options);
1305
+ return response.value as T;
1306
+ }
1307
+
1308
+ async function readPackageJson(): Promise<Record<string, any>> {
1309
+ const packageJsonPath = plugins.path.join(process.cwd(), "package.json");
1310
+ if (!(await plugins.smartfs.file(packageJsonPath).exists())) {
1311
+ return {};
1312
+ }
1313
+ const content = (await plugins.smartfs
1314
+ .file(packageJsonPath)
1315
+ .encoding("utf8")
1316
+ .read()) as string;
1317
+ return JSON.parse(content);
1318
+ }
1319
+
1320
+ function inferRepoName(packageName: unknown): string {
1321
+ if (typeof packageName === "string" && packageName.trim()) {
1322
+ const normalizedName = packageName.trim();
1323
+ return normalizedName.includes("/")
1324
+ ? normalizedName.split("/").pop() || normalizedName
1325
+ : normalizedName;
1326
+ }
1327
+ return plugins.path.basename(process.cwd());
1328
+ }
1329
+
1330
+ function parseCsv(value: string): string[] {
1331
+ const result: string[] = [];
1332
+ for (const item of value.split(",")) {
1333
+ const trimmedItem = item.trim();
1334
+ if (trimmedItem && !result.includes(trimmedItem)) {
1335
+ result.push(trimmedItem);
1336
+ }
1337
+ }
1338
+ return result;
1339
+ }
1340
+
1341
+ function normalizeRegistryUrl(url: string): string {
1342
+ let normalizedUrl = url.trim();
1343
+ if (!normalizedUrl.startsWith("http://") && !normalizedUrl.startsWith("https://")) {
1344
+ normalizedUrl = `https://${normalizedUrl}`;
1345
+ }
1346
+ return normalizedUrl.endsWith("/") ? normalizedUrl.slice(0, -1) : normalizedUrl;
1347
+ }
1348
+
1349
+ function getDefaultEnabledTargets(currentTargets: Record<string, any>): string[] {
1350
+ const enabledTargets: string[] = [];
1351
+ const npmRegistries = currentTargets.npm?.registries;
1352
+ if (currentTargets.git?.enabled ?? true) {
1353
+ enabledTargets.push("git");
1354
+ }
1355
+ if (currentTargets.npm?.enabled ?? (Array.isArray(npmRegistries) && npmRegistries.length > 0)) {
1356
+ enabledTargets.push("npm");
1357
+ }
1358
+ if (currentTargets.docker?.enabled ?? false) {
1359
+ enabledTargets.push("docker");
1360
+ }
1361
+ return enabledTargets;
1362
+ }
1363
+
1364
+ function countDoctorFindings(
1365
+ findings: IDoctorFinding[],
1366
+ ): Record<TDoctorFindingLevel, number> {
1367
+ return findings.reduce(
1368
+ (accumulator, finding) => {
1369
+ accumulator[finding.level] += 1;
1370
+ return accumulator;
1371
+ },
1372
+ { ok: 0, warn: 0, error: 0 } as Record<TDoctorFindingLevel, number>,
1373
+ );
1374
+ }
1375
+
1376
+ function buildConfigFixPrompt(
1377
+ findings: IDoctorFinding[],
1378
+ extraInstructions: string,
1379
+ ): string {
1380
+ const promptParts = [
1381
+ "Other /c-* commands can be found at ~/.config/opencode/commands/*",
1382
+ "# gitzone config fix",
1383
+ "",
1384
+ `Working directory: ${process.cwd()}`,
1385
+ "",
1386
+ "Repair the project configuration so `gitzone config doctor --json` passes.",
1387
+ "",
1388
+ "Rules:",
1389
+ "- Read `.smartconfig.json`, `package.json`, and nearby project metadata before editing.",
1390
+ "- Keep gitzone CLI config under `@git.zone/cli` in `.smartconfig.json`.",
1391
+ `- Use schemaVersion ${CURRENT_GITZONE_CLI_SCHEMA_VERSION} for ` +
1392
+ "`@git.zone/cli`.",
1393
+ "- Use target-based release config: `release.targets.git`, `release.targets.npm`, and `release.targets.docker`.",
1394
+ "- Keep npm registries only at `@git.zone/cli.release.targets.npm.registries`.",
1395
+ "- Do not add runtime legacy compatibility code. If legacy config exists, migrate it explicitly.",
1396
+ "- Do not commit, release, install dependencies, or modify unrelated files.",
1397
+ "- Use pnpm commands only if commands are needed.",
1398
+ "- Run `gitzone config doctor --json` after changes and keep fixing until no errors remain.",
1399
+ "- Run `git diff --check` after changes to catch whitespace problems.",
1400
+ "",
1401
+ "Current doctor findings:",
1402
+ JSON.stringify(findings, null, 2),
1403
+ ];
1404
+
1405
+ if (extraInstructions) {
1406
+ promptParts.push("", "Additional user instructions:", extraInstructions);
1407
+ }
1408
+
1409
+ return promptParts.join("\n");
1410
+ }
1411
+
1412
+ function printDoctorResult(findings: IDoctorFinding[], mode: ICliMode): void {
1413
+ const counts = countDoctorFindings(findings);
1414
+
1415
+ if (mode.json) {
1416
+ printJson({
1417
+ ok: counts.error === 0,
1418
+ counts,
1419
+ findings,
1420
+ });
1421
+ } else {
1422
+ console.log("");
1423
+ console.log("gitzone config doctor");
1424
+ console.log("");
1425
+ for (const finding of findings) {
1426
+ const prefix = finding.level.toUpperCase().padEnd(5);
1427
+ console.log(`${prefix} ${finding.message}`);
1428
+ if (finding.fix) {
1429
+ console.log(` ${finding.fix}`);
1430
+ }
1431
+ }
1432
+ console.log("");
1433
+ console.log(`Summary: ${counts.ok} ok, ${counts.warn} warning, ${counts.error} error`);
1434
+ console.log("");
1435
+ }
1436
+
1437
+ if (counts.error > 0) {
1438
+ process.exitCode = 1;
1439
+ }
1440
+ }
1441
+
1442
+ function validateCommitConfig(
1443
+ commitConfig: Record<string, any>,
1444
+ findings: IDoctorFinding[],
1445
+ ): void {
1446
+ const confirmation = commitConfig.confirmation;
1447
+ if (confirmation === undefined || validConfirmationModes.includes(confirmation)) {
1448
+ findings.push({ level: "ok", message: "Commit confirmation mode is valid" });
1449
+ } else {
1450
+ findings.push({
1451
+ level: "warn",
1452
+ message: `Invalid commit confirmation mode: ${formatValue(confirmation)}`,
1453
+ fix: "Use prompt, auto, or plan.",
1454
+ });
1455
+ }
1456
+
1457
+ const steps = commitConfig.steps;
1458
+ if (steps === undefined) {
1459
+ findings.push({ level: "ok", message: "Commit workflow uses default steps" });
1460
+ return;
1461
+ }
1462
+ if (!Array.isArray(steps) || steps.length === 0) {
1463
+ findings.push({
1464
+ level: "error",
1465
+ message: "Commit steps must be a non-empty array",
1466
+ fix: "Run `gitzone config commit` or unset commit.steps.",
1467
+ });
1468
+ return;
1469
+ }
1470
+
1471
+ const invalidSteps = steps.filter((step) => !validCommitSteps.includes(step));
1472
+ if (invalidSteps.length > 0) {
1473
+ findings.push({
1474
+ level: "error",
1475
+ message: `Invalid commit steps: ${invalidSteps.join(", ")}`,
1476
+ fix: `Allowed steps: ${validCommitSteps.join(", ")}`,
1477
+ });
1478
+ }
1479
+
1480
+ const analyzeIndex = steps.indexOf("analyze");
1481
+ const changelogIndex = steps.indexOf("changelog");
1482
+ const commitIndex = steps.indexOf("commit");
1483
+ if (analyzeIndex === -1 || changelogIndex === -1 || commitIndex === -1) {
1484
+ findings.push({
1485
+ level: "error",
1486
+ message: "Commit workflow must include analyze, changelog, and commit",
1487
+ fix: "Run `gitzone config commit` or reset commit.steps.",
1488
+ });
1489
+ } else if (analyzeIndex > commitIndex || changelogIndex > commitIndex) {
1490
+ findings.push({
1491
+ level: "error",
1492
+ message: "Commit workflow must run analyze and changelog before commit",
1493
+ fix: "Move analyze and changelog before commit in commit.steps.",
1494
+ });
1495
+ } else {
1496
+ findings.push({ level: "ok", message: "Commit workflow steps are valid" });
1497
+ }
1498
+
1499
+ if (steps.includes("test") && commitConfig.test?.command === "") {
1500
+ findings.push({
1501
+ level: "warn",
1502
+ message: "Commit test step has an empty command",
1503
+ fix: "Set commit.test.command or unset it to use the default pnpm test.",
1504
+ });
1505
+ }
1506
+ if (steps.includes("build") && commitConfig.build?.command === "") {
1507
+ findings.push({
1508
+ level: "warn",
1509
+ message: "Commit build step has an empty command",
1510
+ fix: "Set commit.build.command or unset it to use the default pnpm build.",
1511
+ });
1512
+ }
1513
+ }
1514
+
1515
+ async function validateReleaseConfig(
1516
+ releaseConfig: Record<string, any>,
1517
+ findings: IDoctorFinding[],
1518
+ ): Promise<void> {
1519
+ const confirmation = releaseConfig.confirmation;
1520
+ if (confirmation === undefined || validConfirmationModes.includes(confirmation)) {
1521
+ findings.push({ level: "ok", message: "Release confirmation mode is valid" });
1522
+ } else {
1523
+ findings.push({
1524
+ level: "warn",
1525
+ message: `Invalid release confirmation mode: ${formatValue(confirmation)}`,
1526
+ fix: "Use prompt, auto, or plan.",
1527
+ });
1528
+ }
1529
+
1530
+ if (releaseConfig.registries || releaseConfig.accessLevel || releaseConfig.steps || releaseConfig.changelog) {
1531
+ findings.push({
1532
+ level: "warn",
1533
+ message: "Legacy release keys are present outside release.targets",
1534
+ fix: "Run `gitzone config migrate`.",
1535
+ });
1536
+ }
1537
+
1538
+ const preflight = releaseConfig.preflight || {};
1539
+ if (preflight.test === true && preflight.testCommand === "") {
1540
+ findings.push({
1541
+ level: "warn",
1542
+ message: "Release test preflight has an empty command",
1543
+ fix: "Set release.preflight.testCommand or unset it to use pnpm test.",
1544
+ });
1545
+ }
1546
+ if (preflight.build === true && preflight.buildCommand === "") {
1547
+ findings.push({
1548
+ level: "warn",
1549
+ message: "Release build preflight has an empty command",
1550
+ fix: "Set release.preflight.buildCommand or unset it to use pnpm build.",
1551
+ });
1552
+ }
1553
+
1554
+ const targets = releaseConfig.targets || {};
1555
+ await validateGitTarget(targets.git || {}, findings);
1556
+ await validateNpmTarget(targets.npm || {}, findings);
1557
+ validateDockerTarget(targets.docker || {}, findings);
1558
+ }
1559
+
1560
+ async function validateGitTarget(
1561
+ gitTarget: Record<string, any>,
1562
+ findings: IDoctorFinding[],
1563
+ ): Promise<void> {
1564
+ const enabled = gitTarget.enabled ?? true;
1565
+ if (!enabled) {
1566
+ findings.push({ level: "ok", message: "Git release target is disabled" });
1567
+ return;
1568
+ }
1569
+
1570
+ if (gitTarget.remote === "") {
1571
+ findings.push({
1572
+ level: "error",
1573
+ message: "Git release target remote is empty",
1574
+ fix: "Set release.targets.git.remote or unset it to use origin.",
1575
+ });
1576
+ return;
1577
+ }
1578
+ findings.push({
1579
+ level: "ok",
1580
+ message: `Git release target is enabled (${gitTarget.remote || "origin"})`,
1581
+ });
1582
+ }
1583
+
1584
+ async function validateNpmTarget(
1585
+ npmTarget: Record<string, any>,
1586
+ findings: IDoctorFinding[],
1587
+ ): Promise<void> {
1588
+ const registries = Array.isArray(npmTarget.registries) ? npmTarget.registries : [];
1589
+ const enabled = npmTarget.enabled ?? registries.length > 0;
1590
+ if (!enabled) {
1591
+ findings.push({ level: "ok", message: "npm release target is disabled" });
1592
+ return;
1593
+ }
1594
+
1595
+ if (registries.length === 0) {
1596
+ findings.push({
1597
+ level: "error",
1598
+ message: "npm release target is enabled without registries",
1599
+ fix: "Run `gitzone config add https://registry.npmjs.org` or disable release.targets.npm.enabled.",
1600
+ });
1601
+ return;
1602
+ }
1603
+
1604
+ if (npmTarget.accessLevel && !["public", "private"].includes(npmTarget.accessLevel)) {
1605
+ findings.push({
1606
+ level: "error",
1607
+ message: `Invalid npm access level: ${npmTarget.accessLevel}`,
1608
+ fix: "Use public or private.",
1609
+ });
1610
+ }
1611
+ if (npmTarget.alreadyPublished && !["success", "error"].includes(npmTarget.alreadyPublished)) {
1612
+ findings.push({
1613
+ level: "error",
1614
+ message: `Invalid npm alreadyPublished behavior: ${npmTarget.alreadyPublished}`,
1615
+ fix: "Use success or error.",
1616
+ });
1617
+ }
1618
+
1619
+ const smartNetwork = new plugins.smartnetwork.SmartNetwork();
1620
+ await Promise.all(
1621
+ registries.map(async (registry) => {
1622
+ await validateNpmRegistry(registry, smartNetwork, findings);
1623
+ await validateNpmAuth(registry, findings);
1624
+ }),
1625
+ );
1626
+ }
1627
+
1628
+ async function validateNpmRegistry(
1629
+ registry: string,
1630
+ smartNetwork: plugins.smartnetwork.SmartNetwork,
1631
+ findings: IDoctorFinding[],
1632
+ ): Promise<void> {
1633
+ const normalizedRegistry = normalizeRegistryUrl(registry);
1634
+ if (normalizedRegistry !== registry) {
1635
+ findings.push({
1636
+ level: "warn",
1637
+ message: `npm registry should be normalized: ${registry}`,
1638
+ fix: `Use ${normalizedRegistry}`,
1639
+ });
1640
+ }
1641
+
1642
+ let registryUrl: URL;
1643
+ try {
1644
+ registryUrl = new URL(normalizedRegistry);
1645
+ } catch {
1646
+ findings.push({
1647
+ level: "error",
1648
+ message: `Invalid npm registry URL: ${registry}`,
1649
+ fix: "Use a valid http or https registry URL.",
1650
+ });
1651
+ return;
1652
+ }
1653
+
1654
+ if (registryUrl.protocol !== "https:" && registryUrl.protocol !== "http:") {
1655
+ findings.push({
1656
+ level: "error",
1657
+ message: `Unsupported npm registry protocol: ${registryUrl.protocol}`,
1658
+ fix: "Use an http or https registry URL.",
1659
+ });
1660
+ return;
1661
+ }
1662
+
1663
+ try {
1664
+ const result = await smartNetwork.checkEndpoint(normalizedRegistry, { timeout: 5000 });
1665
+ if (result.status >= 200 && result.status < 500) {
1666
+ findings.push({
1667
+ level: "ok",
1668
+ message: `npm registry is reachable: ${normalizedRegistry} (${result.status})`,
1669
+ });
1670
+ } else {
1671
+ findings.push({
1672
+ level: "warn",
1673
+ message: `npm registry returned status ${result.status}: ${normalizedRegistry}`,
1674
+ });
1675
+ }
1676
+ } catch (error) {
1677
+ findings.push({
1678
+ level: "warn",
1679
+ message: `npm registry is not reachable: ${normalizedRegistry}`,
1680
+ fix: error instanceof Error ? error.message : String(error),
1681
+ });
1682
+ }
1683
+ }
1684
+
1685
+ async function validateNpmAuth(
1686
+ registry: string,
1687
+ findings: IDoctorFinding[],
1688
+ ): Promise<void> {
1689
+ const normalizedRegistry = normalizeRegistryUrl(registry);
1690
+ const smartshellInstance = new plugins.smartshell.Smartshell({
1691
+ executor: "bash",
1692
+ sourceFilePaths: [],
1693
+ });
1694
+ try {
1695
+ const result = await smartshellInstance.execSpawn(
1696
+ "pnpm",
1697
+ ["npm", "whoami", `--registry=${normalizedRegistry}`],
1698
+ { silent: true, timeout: 8000 },
1699
+ );
1700
+ if (result.exitCode === 0) {
1701
+ findings.push({
1702
+ level: "ok",
1703
+ message: `npm auth is available for ${normalizedRegistry}`,
1704
+ });
1705
+ } else {
1706
+ findings.push({
1707
+ level: "warn",
1708
+ message: `npm auth is missing or invalid for ${normalizedRegistry}`,
1709
+ fix: `Run pnpm npm login --registry=${normalizedRegistry}`,
1710
+ });
1711
+ }
1712
+ } catch (error) {
1713
+ findings.push({
1714
+ level: "warn",
1715
+ message: `Could not check npm auth for ${normalizedRegistry}`,
1716
+ fix: error instanceof Error ? error.message : String(error),
1717
+ });
1718
+ }
1719
+ }
1720
+
1721
+ function validateDockerTarget(
1722
+ dockerTarget: Record<string, any>,
1723
+ findings: IDoctorFinding[],
1724
+ ): void {
1725
+ const enabled = dockerTarget.enabled ?? false;
1726
+ if (!enabled) {
1727
+ findings.push({ level: "ok", message: "Docker release target is disabled" });
1728
+ return;
1729
+ }
1730
+
1731
+ if (!Array.isArray(dockerTarget.images) || dockerTarget.images.length === 0) {
1732
+ findings.push({
1733
+ level: "error",
1734
+ message: "Docker release target is enabled without images",
1735
+ fix: "Set release.targets.docker.images or disable release.targets.docker.enabled.",
1736
+ });
1737
+ return;
1738
+ }
1739
+
1740
+ findings.push({
1741
+ level: "ok",
1742
+ message: `Docker release target has ${dockerTarget.images.length} image template(s)`,
1743
+ });
1744
+ }
1745
+
1746
+ async function validateDetectedProjectType(
1747
+ cliConfig: Record<string, any>,
1748
+ findings: IDoctorFinding[],
1749
+ ): Promise<void> {
1750
+ const packageJsonPath = plugins.path.join(process.cwd(), "package.json");
1751
+ const denoJsonPath = plugins.path.join(process.cwd(), "deno.json");
1752
+ const hasPackageJson = await plugins.smartfs.file(packageJsonPath).exists();
1753
+ const hasDenoJson = await plugins.smartfs.file(denoJsonPath).exists();
1754
+
1755
+ if (!hasPackageJson && !hasDenoJson) {
1756
+ findings.push({
1757
+ level: "warn",
1758
+ message: "Could not detect package.json or deno.json",
1759
+ fix: "Run this command from a project root.",
1760
+ });
1761
+ return;
1762
+ }
1763
+
1764
+ if (hasPackageJson && validProjectTypes.includes(cliConfig.projectType)) {
1765
+ findings.push({
1766
+ level: "ok",
1767
+ message: "Detected project files match configured npm-compatible project type",
1768
+ });
1769
+ return;
1770
+ }
1771
+
1772
+ if (hasDenoJson && !hasPackageJson) {
1773
+ findings.push({
1774
+ level: "warn",
1775
+ message: "Detected a Deno-only project, but guided config supports npm-compatible project types",
1776
+ fix: "Use `gitzone config set projectType npm|service|wcc|website` only for npm-compatible projects.",
1777
+ });
1778
+ }
1779
+ }
1780
+
718
1781
  /**
719
1782
  * Show help for config command
720
1783
  */
@@ -728,6 +1791,11 @@ export function showHelp(mode?: ICliMode): void {
728
1791
  name: "show",
729
1792
  description: "Display current @git.zone/cli configuration",
730
1793
  },
1794
+ { name: "project", description: "Configure project basics interactively" },
1795
+ { name: "cli", description: "Configure CLI behavior interactively" },
1796
+ { name: "release", description: "Configure release workflow interactively" },
1797
+ { name: "doctor", description: "Validate .smartconfig.json" },
1798
+ { name: "fix [instructions]", description: "Use opencode to repair .smartconfig.json" },
731
1799
  { name: "get <path>", description: "Read a single config value" },
732
1800
  { name: "set <path> <value>", description: "Write a config value" },
733
1801
  { name: "unset <path>", description: "Delete a config value" },
@@ -749,6 +1817,10 @@ export function showHelp(mode?: ICliMode): void {
749
1817
  ],
750
1818
  examples: [
751
1819
  "gitzone config show --json",
1820
+ "gitzone config project",
1821
+ "gitzone config doctor --json",
1822
+ "gitzone config fix",
1823
+ "gitzone config fix -y",
752
1824
  "gitzone config get release.targets.npm.accessLevel",
753
1825
  "gitzone config set cli.interactive false",
754
1826
  "gitzone config set cli.output json",
@@ -764,6 +1836,11 @@ export function showHelp(mode?: ICliMode): void {
764
1836
  console.log(
765
1837
  " show Display current @git.zone/cli configuration",
766
1838
  );
1839
+ console.log(" project Configure project basics interactively");
1840
+ console.log(" cli Configure CLI behavior interactively");
1841
+ console.log(" release Configure release workflow interactively");
1842
+ console.log(" doctor Validate .smartconfig.json");
1843
+ console.log(" fix [instructions] Use opencode to repair .smartconfig.json");
767
1844
  console.log(" get <path> Read a single config value");
768
1845
  console.log(" set <path> <value> Write a config value");
769
1846
  console.log(" unset <path> Delete a config value");
@@ -782,6 +1859,12 @@ export function showHelp(mode?: ICliMode): void {
782
1859
  console.log("Examples:");
783
1860
  console.log(" gitzone config show");
784
1861
  console.log(" gitzone config show --json");
1862
+ console.log(" gitzone config project");
1863
+ console.log(" gitzone config cli");
1864
+ console.log(" gitzone config release");
1865
+ console.log(" gitzone config doctor --json");
1866
+ console.log(" gitzone config fix");
1867
+ console.log(" gitzone config fix -y");
785
1868
  console.log(" gitzone config get release.targets.npm.accessLevel");
786
1869
  console.log(" gitzone config set cli.interactive false");
787
1870
  console.log(" gitzone config set cli.output json");