@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/mod_config/index.js +978 -26
- package/package.json +1 -1
- package/readme.md +12 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/mod_config/index.ts +1109 -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,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
|
|
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
|
-
|
|
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
|
-
"│
|
|
246
|
+
"│ gitzone config - Project Configuration │",
|
|
212
247
|
);
|
|
213
248
|
console.log(
|
|
214
249
|
"╰─────────────────────────────────────────────────────────────╯",
|
|
215
250
|
);
|
|
216
251
|
console.log("");
|
|
217
252
|
|
|
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
|
-
});
|
|
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
|
-
|
|
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");
|