@git.zone/cli 2.14.2 → 2.15.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.
Files changed (46) hide show
  1. package/.smartconfig.json +36 -11
  2. package/assets/templates/service/npmextra.json +8 -2
  3. package/assets/templates/smartconfig/_smartconfig.json +5 -1
  4. package/assets/templates/website/npmextra.json +8 -2
  5. package/dist_ts/00_commitinfo_data.js +1 -1
  6. package/dist_ts/gitzone.cli.js +8 -1
  7. package/dist_ts/helpers.changelog.d.ts +16 -0
  8. package/dist_ts/helpers.changelog.js +114 -0
  9. package/dist_ts/helpers.smartconfigmigrations.d.ts +7 -0
  10. package/dist_ts/helpers.smartconfigmigrations.js +161 -0
  11. package/dist_ts/helpers.workflow.d.ts +97 -0
  12. package/dist_ts/helpers.workflow.js +258 -0
  13. package/dist_ts/mod_commit/index.js +190 -303
  14. package/dist_ts/mod_commit/mod.helpers.d.ts +23 -0
  15. package/dist_ts/mod_commit/mod.helpers.js +22 -15
  16. package/dist_ts/mod_commit/mod.ui.d.ts +1 -1
  17. package/dist_ts/mod_commit/mod.ui.js +7 -2
  18. package/dist_ts/mod_config/classes.commitconfig.d.ts +6 -0
  19. package/dist_ts/mod_config/classes.commitconfig.js +28 -4
  20. package/dist_ts/mod_config/classes.releaseconfig.js +13 -7
  21. package/dist_ts/mod_config/index.js +77 -29
  22. package/dist_ts/mod_format/formatters/smartconfig.formatter.js +3 -57
  23. package/dist_ts/mod_release/index.d.ts +3 -0
  24. package/dist_ts/mod_release/index.js +299 -0
  25. package/dist_ts/mod_release/mod.plugins.d.ts +3 -0
  26. package/dist_ts/mod_release/mod.plugins.js +4 -0
  27. package/dist_ts/mod_standard/index.js +16 -3
  28. package/license +2 -2
  29. package/package.json +20 -30
  30. package/readme.hints.md +15 -17
  31. package/readme.md +239 -421
  32. package/ts/00_commitinfo_data.ts +1 -1
  33. package/ts/gitzone.cli.ts +8 -0
  34. package/ts/helpers.changelog.ts +165 -0
  35. package/ts/helpers.smartconfigmigrations.ts +192 -0
  36. package/ts/helpers.workflow.ts +387 -0
  37. package/ts/mod_commit/index.ts +233 -435
  38. package/ts/mod_commit/mod.helpers.ts +28 -16
  39. package/ts/mod_commit/mod.ui.ts +7 -2
  40. package/ts/mod_config/classes.commitconfig.ts +33 -3
  41. package/ts/mod_config/classes.releaseconfig.ts +14 -7
  42. package/ts/mod_config/index.ts +89 -28
  43. package/ts/mod_format/formatters/smartconfig.formatter.ts +2 -62
  44. package/ts/mod_release/index.ts +393 -0
  45. package/ts/mod_release/mod.plugins.ts +5 -0
  46. package/ts/mod_standard/index.ts +15 -2
@@ -3,15 +3,11 @@
3
3
  import * as plugins from "./mod.plugins.js";
4
4
  import * as paths from "../paths.js";
5
5
  import { logger } from "../gitzone.logging.js";
6
- import * as helpers from "./mod.helpers.js";
7
6
  import * as ui from "./mod.ui.js";
8
- import { ReleaseConfig } from "../mod_config/classes.releaseconfig.js";
9
7
  import type { ICliMode } from "../helpers.climode.js";
10
- import {
11
- getCliMode,
12
- printJson,
13
- runWithSuppressedOutput,
14
- } from "../helpers.climode.js";
8
+ import { getCliMode, printJson, runWithSuppressedOutput } from "../helpers.climode.js";
9
+ import { appendPendingChangelogEntry } from "../helpers.changelog.js";
10
+ import { resolveCommitWorkflow, type IResolvedCommitWorkflow } from "../helpers.workflow.js";
15
11
 
16
12
  export const run = async (argvArg: any) => {
17
13
  const mode = await getCliMode(argvArg);
@@ -36,430 +32,246 @@ export const run = async (argvArg: any) => {
36
32
  return;
37
33
  }
38
34
 
39
- // Read commit config from .smartconfig.json
40
- const smartconfigInstance = new plugins.smartconfig.Smartconfig();
41
- const gitzoneConfig = smartconfigInstance.dataFor<{
42
- commit?: {
43
- alwaysTest?: boolean;
44
- alwaysBuild?: boolean;
45
- };
46
- }>("@git.zone/cli", {});
47
- const commitConfig = gitzoneConfig.commit || {};
48
-
49
- // Check flags and merge with config options
50
- const wantsRelease = !!(argvArg.r || argvArg.release);
51
- const wantsTest = !!(argvArg.t || argvArg.test || commitConfig.alwaysTest);
52
- const wantsBuild = !!(argvArg.b || argvArg.build || commitConfig.alwaysBuild);
53
- let releaseConfig: ReleaseConfig | null = null;
54
-
55
- if (wantsRelease) {
56
- releaseConfig = await ReleaseConfig.fromCwd();
57
- if (!releaseConfig.hasRegistries()) {
58
- logger.log("error", "No release registries configured.");
59
- console.log("");
60
- console.log(
61
- " Run `gitzone config add <registry-url>` to add registries.",
62
- );
63
- console.log("");
64
- process.exit(1);
65
- }
35
+ const workflow = await resolveCommitWorkflow(argvArg);
36
+ if (workflow.releaseFlagRequested) {
37
+ logger.log(
38
+ "warn",
39
+ "`gitzone commit -r` is deprecated and no longer releases. Use `gitzone release` after committing.",
40
+ );
66
41
  }
67
42
 
68
- // Print execution plan at the start
69
- ui.printExecutionPlan({
70
- autoAccept: !!(argvArg.y || argvArg.yes),
71
- push: !!(argvArg.p || argvArg.push),
72
- test: wantsTest,
73
- build: wantsBuild,
74
- release: wantsRelease,
75
- format: !!argvArg.format,
76
- registries: releaseConfig?.getRegistries(),
77
- });
78
-
79
- if (argvArg.format) {
80
- const formatMod = await import("../mod_format/index.js");
81
- await formatMod.run();
43
+ printCommitExecutionPlan(workflow);
44
+ if (workflow.confirmation === "plan") {
45
+ return;
82
46
  }
83
47
 
84
- // Run tests early to fail fast before analysis
85
- if (wantsTest) {
86
- ui.printHeader("🧪 Running tests...");
87
- const smartshellForTest = new plugins.smartshell.Smartshell({
88
- executor: "bash",
89
- sourceFilePaths: [],
90
- });
91
- const testResult = await smartshellForTest.exec("pnpm test");
92
- if (testResult.exitCode !== 0) {
93
- logger.log("error", "Tests failed. Aborting commit.");
94
- process.exit(1);
48
+ const smartshellInstance = new plugins.smartshell.Smartshell({
49
+ executor: "bash",
50
+ sourceFilePaths: [],
51
+ });
52
+
53
+ let nextCommitObject: any;
54
+ let answerBucket: plugins.smartinteract.AnswerBucket | undefined;
55
+
56
+ for (const step of workflow.steps) {
57
+ switch (step) {
58
+ case "format":
59
+ await runFormatStep();
60
+ break;
61
+ case "test":
62
+ await runCommandStep(smartshellInstance, "Running tests", workflow.testCommand);
63
+ break;
64
+ case "build":
65
+ await runCommandStep(smartshellInstance, "Running build", workflow.buildCommand);
66
+ break;
67
+ case "analyze":
68
+ nextCommitObject = await runAnalyzeStep();
69
+ answerBucket = await buildAnswerBucket(nextCommitObject, workflow, mode, argvArg);
70
+ break;
71
+ case "changelog":
72
+ assertAnalysisComplete(answerBucket, nextCommitObject);
73
+ await runChangelogStep(workflow, answerBucket!, nextCommitObject);
74
+ break;
75
+ case "commit":
76
+ assertAnalysisComplete(answerBucket, nextCommitObject);
77
+ await runCommitStep(smartshellInstance, answerBucket!);
78
+ break;
79
+ case "push":
80
+ await runPushStep(smartshellInstance, workflow);
81
+ break;
95
82
  }
96
- logger.log("success", "All tests passed.");
97
83
  }
98
84
 
99
- ui.printHeader("🔍 Analyzing repository changes...");
100
-
101
- const aidoc = new plugins.tsdoc.AiDoc();
102
- await aidoc.start();
103
-
104
- const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
85
+ const commitShaResult = await smartshellInstance.exec("git rev-parse --short HEAD");
86
+ const currentBranch = await detectCurrentBranch(smartshellInstance);
87
+ ui.printSummary({
88
+ projectType: "source",
89
+ branch: currentBranch,
90
+ commitType: answerBucket!.getAnswerFor("commitType"),
91
+ commitScope: answerBucket!.getAnswerFor("commitScope"),
92
+ commitMessage: answerBucket!.getAnswerFor("commitDescription"),
93
+ commitSha: commitShaResult.stdout.trim(),
94
+ pushed: workflow.steps.includes("push"),
95
+ });
96
+ };
105
97
 
106
- await aidoc.stop();
98
+ async function runFormatStep(): Promise<void> {
99
+ ui.printHeader("Formatting project files");
100
+ const formatMod = await import("../mod_format/index.js");
101
+ await formatMod.run({ write: true, yes: true, interactive: false });
102
+ }
107
103
 
108
- ui.printRecommendation({
109
- recommendedNextVersion: nextCommitObject.recommendedNextVersion,
110
- recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
111
- recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
112
- recommendedNextVersionMessage:
113
- nextCommitObject.recommendedNextVersionMessage,
114
- });
104
+ async function runCommandStep(
105
+ smartshellInstance: plugins.smartshell.Smartshell,
106
+ label: string,
107
+ command: string,
108
+ ): Promise<void> {
109
+ ui.printHeader(label);
110
+ const result = await smartshellInstance.exec(command);
111
+ if (result.exitCode !== 0) {
112
+ logger.log("error", `${label} failed. Aborting commit.`);
113
+ process.exit(1);
114
+ }
115
+ logger.log("success", `${label} passed.`);
116
+ }
115
117
 
116
- let answerBucket: plugins.smartinteract.AnswerBucket;
118
+ async function runAnalyzeStep(): Promise<any> {
119
+ ui.printHeader("Analyzing repository changes");
120
+ const aidoc = new plugins.tsdoc.AiDoc();
121
+ await aidoc.start();
122
+ try {
123
+ const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
124
+ ui.printRecommendation({
125
+ recommendedNextVersion: nextCommitObject.recommendedNextVersion,
126
+ recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
127
+ recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
128
+ recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage,
129
+ });
130
+ return nextCommitObject;
131
+ } finally {
132
+ await aidoc.stop();
133
+ }
134
+ }
117
135
 
118
- // Check if -y/--yes flag is set AND version is not a breaking change
119
- // Breaking changes (major version bumps) always require manual confirmation
120
- const isBreakingChange =
121
- nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
122
- const canAutoAccept = (argvArg.y || argvArg.yes) && !isBreakingChange;
136
+ async function buildAnswerBucket(
137
+ nextCommitObject: any,
138
+ workflow: IResolvedCommitWorkflow,
139
+ mode: ICliMode,
140
+ argvArg: any,
141
+ ): Promise<plugins.smartinteract.AnswerBucket> {
142
+ const isBreakingChange = nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
143
+ const canAutoAccept = workflow.confirmation === "auto" && !isBreakingChange;
123
144
 
124
145
  if (canAutoAccept) {
125
- // Auto-mode: create AnswerBucket programmatically
126
- logger.log("info", "✓ Auto-accepting AI recommendations (--yes flag)");
127
-
128
- answerBucket = new plugins.smartinteract.AnswerBucket();
129
- answerBucket.addAnswer({
130
- name: "commitType",
131
- value: nextCommitObject.recommendedNextVersionLevel,
132
- });
133
- answerBucket.addAnswer({
134
- name: "commitScope",
135
- value: nextCommitObject.recommendedNextVersionScope,
136
- });
137
- answerBucket.addAnswer({
138
- name: "commitDescription",
139
- value: nextCommitObject.recommendedNextVersionMessage,
140
- });
141
- answerBucket.addAnswer({
142
- name: "pushToOrigin",
143
- value: !!(argvArg.p || argvArg.push), // Only push if -p flag also provided
146
+ logger.log("info", "Auto-accepting AI recommendations");
147
+ return createAnswerBucket({
148
+ commitType: nextCommitObject.recommendedNextVersionLevel,
149
+ commitScope: nextCommitObject.recommendedNextVersionScope,
150
+ commitDescription: nextCommitObject.recommendedNextVersionMessage,
144
151
  });
145
- answerBucket.addAnswer({
146
- name: "createRelease",
147
- value: wantsRelease,
148
- });
149
- } else {
150
- // Warn if --yes was provided but we're requiring confirmation due to breaking change
151
- if (isBreakingChange && (argvArg.y || argvArg.yes)) {
152
- logger.log(
153
- "warn",
154
- "⚠️ BREAKING CHANGE detected - manual confirmation required",
155
- );
156
- }
157
- // Interactive mode: prompt user for input
158
- const commitInteract = new plugins.smartinteract.SmartInteract();
159
- commitInteract.addQuestions([
160
- {
161
- type: "list",
162
- name: `commitType`,
163
- message: `Choose TYPE of the commit:`,
164
- choices: [`fix`, `feat`, `BREAKING CHANGE`],
165
- default: nextCommitObject.recommendedNextVersionLevel,
166
- },
167
- {
168
- type: "input",
169
- name: `commitScope`,
170
- message: `What is the SCOPE of the commit:`,
171
- default: nextCommitObject.recommendedNextVersionScope,
172
- },
173
- {
174
- type: `input`,
175
- name: `commitDescription`,
176
- message: `What is the DESCRIPTION of the commit?`,
177
- default: nextCommitObject.recommendedNextVersionMessage,
178
- },
179
- {
180
- type: "confirm",
181
- name: `pushToOrigin`,
182
- message: `Do you want to push this version now?`,
183
- default: true,
184
- },
185
- {
186
- type: "confirm",
187
- name: `createRelease`,
188
- message: `Do you want to publish to npm registries?`,
189
- default: wantsRelease,
190
- },
191
- ]);
192
- answerBucket = await commitInteract.runQueue();
193
- }
194
- const commitString = createCommitStringFromAnswerBucket(answerBucket);
195
- const commitType = answerBucket.getAnswerFor("commitType");
196
- let commitVersionType: helpers.VersionType;
197
- switch (commitType) {
198
- case "fix":
199
- commitVersionType = "patch";
200
- break;
201
- case "feat":
202
- commitVersionType = "minor";
203
- break;
204
- case "BREAKING CHANGE":
205
- commitVersionType = "major";
206
- break;
207
- default:
208
- throw new Error(`Unsupported commit type: ${commitType}`);
209
152
  }
210
153
 
211
- ui.printHeader(" Creating Semantic Commit");
212
- ui.printCommitMessage(commitString);
213
- const smartshellInstance = new plugins.smartshell.Smartshell({
214
- executor: "bash",
215
- sourceFilePaths: [],
216
- });
154
+ if (isBreakingChange && (workflow.confirmation === "auto" || argvArg.y || argvArg.yes)) {
155
+ logger.log("warn", "BREAKING CHANGE detected - manual confirmation required");
156
+ }
217
157
 
218
- // Load release config if user wants to release (interactively selected)
219
- if (answerBucket.getAnswerFor("createRelease") && !releaseConfig) {
220
- releaseConfig = await ReleaseConfig.fromCwd();
221
- if (!releaseConfig.hasRegistries()) {
222
- logger.log("error", "No release registries configured.");
223
- console.log("");
224
- console.log(
225
- " Run `gitzone config add <registry-url>` to add registries.",
226
- );
227
- console.log("");
228
- process.exit(1);
229
- }
158
+ if (!mode.interactive) {
159
+ throw new Error("Commit confirmation requires an interactive terminal. Use `-y` or set commit.confirmation to `auto`.");
230
160
  }
231
161
 
232
- // Determine total steps based on options
233
- // Note: test runs early (like format) so not counted in numbered steps
234
- const willPush =
235
- answerBucket.getAnswerFor("pushToOrigin") && !(process.env.CI === "true");
236
- const willRelease =
237
- answerBucket.getAnswerFor("createRelease") &&
238
- releaseConfig?.hasRegistries();
239
- let totalSteps = 5; // Base steps: commitinfo, changelog, staging, commit, version
240
- if (wantsBuild) totalSteps += 2; // build step + verification step
241
- if (willPush) totalSteps++;
242
- if (willRelease) totalSteps++;
243
- let currentStep = 0;
244
-
245
- // Step 1: Baking commitinfo
246
- currentStep++;
247
- ui.printStep(
248
- currentStep,
249
- totalSteps,
250
- "🔧 Baking commit info into code",
251
- "in-progress",
252
- );
253
- const commitInfo = new plugins.commitinfo.CommitInfo(
254
- paths.cwd,
255
- commitVersionType,
256
- );
257
- await commitInfo.writeIntoPotentialDirs();
258
- ui.printStep(
259
- currentStep,
260
- totalSteps,
261
- "🔧 Baking commit info into code",
262
- "done",
263
- );
162
+ const commitInteract = new plugins.smartinteract.SmartInteract();
163
+ commitInteract.addQuestions([
164
+ {
165
+ type: "list",
166
+ name: "commitType",
167
+ message: "Choose TYPE of the commit:",
168
+ choices: ["fix", "feat", "BREAKING CHANGE"],
169
+ default: nextCommitObject.recommendedNextVersionLevel,
170
+ },
171
+ {
172
+ type: "input",
173
+ name: "commitScope",
174
+ message: "What is the SCOPE of the commit:",
175
+ default: nextCommitObject.recommendedNextVersionScope,
176
+ },
177
+ {
178
+ type: "input",
179
+ name: "commitDescription",
180
+ message: "What is the DESCRIPTION of the commit?",
181
+ default: nextCommitObject.recommendedNextVersionMessage,
182
+ },
183
+ ]);
184
+ return await commitInteract.runQueue();
185
+ }
264
186
 
265
- // Step 2: Writing changelog
266
- currentStep++;
267
- ui.printStep(
268
- currentStep,
269
- totalSteps,
270
- "📄 Generating changelog.md",
271
- "in-progress",
272
- );
273
- let changelog = nextCommitObject.changelog || "# Changelog\n";
274
- changelog = changelog.replaceAll(
275
- "{{nextVersion}}",
276
- (await commitInfo.getNextPlannedVersion()).versionString,
277
- );
278
- changelog = changelog.replaceAll(
279
- "{{nextVersionScope}}",
280
- `${await answerBucket.getAnswerFor("commitType")}(${await answerBucket.getAnswerFor("commitScope")})`,
281
- );
282
- changelog = changelog.replaceAll(
283
- "{{nextVersionMessage}}",
284
- nextCommitObject.recommendedNextVersionMessage,
285
- );
286
- if (nextCommitObject.recommendedNextVersionDetails?.length > 0) {
287
- changelog = changelog.replaceAll(
288
- "{{nextVersionDetails}}",
289
- "- " + nextCommitObject.recommendedNextVersionDetails.join("\n- "),
290
- );
291
- } else {
292
- changelog = changelog.replaceAll("\n{{nextVersionDetails}}", "");
187
+ function createAnswerBucket(answers: {
188
+ commitType: string;
189
+ commitScope: string;
190
+ commitDescription: string;
191
+ }): plugins.smartinteract.AnswerBucket {
192
+ const answerBucket = new plugins.smartinteract.AnswerBucket();
193
+ for (const [name, value] of Object.entries(answers)) {
194
+ answerBucket.addAnswer({ name, value });
293
195
  }
196
+ return answerBucket;
197
+ }
294
198
 
295
- await plugins.smartfs
296
- .file(plugins.path.join(paths.cwd, `changelog.md`))
297
- .encoding("utf8")
298
- .write(changelog);
299
- ui.printStep(currentStep, totalSteps, "📄 Generating changelog.md", "done");
300
-
301
- // Step 3: Staging files
302
- currentStep++;
303
- ui.printStep(currentStep, totalSteps, "📦 Staging files", "in-progress");
304
- await smartshellInstance.exec(`git add -A`);
305
- ui.printStep(currentStep, totalSteps, "📦 Staging files", "done");
306
-
307
- // Step 4: Creating commit
308
- currentStep++;
309
- ui.printStep(
310
- currentStep,
311
- totalSteps,
312
- "💾 Creating git commit",
313
- "in-progress",
314
- );
315
- await smartshellInstance.exec(`git commit -m "${commitString}"`);
316
- ui.printStep(currentStep, totalSteps, "💾 Creating git commit", "done");
317
-
318
- // Step 5: Bumping version
319
- currentStep++;
320
- const projectType = await helpers.detectProjectType();
321
- const newVersion = await helpers.bumpProjectVersion(
322
- projectType,
323
- commitVersionType,
324
- currentStep,
325
- totalSteps,
199
+ async function runChangelogStep(
200
+ workflow: IResolvedCommitWorkflow,
201
+ answerBucket: plugins.smartinteract.AnswerBucket,
202
+ nextCommitObject: any,
203
+ ): Promise<void> {
204
+ await appendPendingChangelogEntry(
205
+ plugins.path.join(paths.cwd, workflow.changelogFile),
206
+ workflow.changelogSection,
207
+ {
208
+ type: answerBucket.getAnswerFor("commitType"),
209
+ scope: answerBucket.getAnswerFor("commitScope"),
210
+ message: answerBucket.getAnswerFor("commitDescription"),
211
+ details: nextCommitObject.recommendedNextVersionDetails || [],
212
+ },
326
213
  );
214
+ logger.log("success", `Updated ${workflow.changelogFile} pending section.`);
215
+ }
327
216
 
328
- // Step 6: Run build (optional)
329
- if (wantsBuild) {
330
- currentStep++;
331
- ui.printStep(currentStep, totalSteps, "🔨 Running build", "in-progress");
332
- const buildResult = await smartshellInstance.exec("pnpm build");
333
- if (buildResult.exitCode !== 0) {
334
- ui.printStep(currentStep, totalSteps, "🔨 Running build", "error");
335
- logger.log("error", "Build failed. Aborting release.");
336
- process.exit(1);
337
- }
338
- ui.printStep(currentStep, totalSteps, "🔨 Running build", "done");
339
-
340
- // Step 7: Verify no uncommitted changes
341
- currentStep++;
342
- ui.printStep(
343
- currentStep,
344
- totalSteps,
345
- "🔍 Verifying clean working tree",
346
- "in-progress",
347
- );
348
- const statusResult = await smartshellInstance.exec(
349
- "git status --porcelain",
350
- );
351
- if (statusResult.stdout.trim() !== "") {
352
- ui.printStep(
353
- currentStep,
354
- totalSteps,
355
- "🔍 Verifying clean working tree",
356
- "error",
357
- );
358
- logger.log(
359
- "error",
360
- "Build produced uncommitted changes. This usually means build output is not gitignored.",
361
- );
362
- logger.log("error", "Uncommitted files:");
363
- console.log(statusResult.stdout);
364
- logger.log(
365
- "error",
366
- "Aborting release. Please ensure build artifacts are in .gitignore",
367
- );
368
- process.exit(1);
369
- }
370
- ui.printStep(
371
- currentStep,
372
- totalSteps,
373
- "🔍 Verifying clean working tree",
374
- "done",
375
- );
217
+ async function runCommitStep(
218
+ smartshellInstance: plugins.smartshell.Smartshell,
219
+ answerBucket: plugins.smartinteract.AnswerBucket,
220
+ ): Promise<void> {
221
+ ui.printHeader("Creating Semantic Commit");
222
+ const commitString = createCommitStringFromAnswerBucket(answerBucket);
223
+ ui.printCommitMessage(commitString);
224
+ await smartshellInstance.exec("git add -A");
225
+ const result = await smartshellInstance.exec(`git commit -m ${shellQuote(commitString)}`);
226
+ if (result.exitCode !== 0) {
227
+ logger.log("error", "git commit failed.");
228
+ process.exit(1);
376
229
  }
230
+ }
377
231
 
378
- // Step: Push to remote (optional)
379
- const currentBranch = await helpers.detectCurrentBranch();
380
- if (willPush) {
381
- currentStep++;
382
- ui.printStep(
383
- currentStep,
384
- totalSteps,
385
- `🚀 Pushing to origin/${currentBranch}`,
386
- "in-progress",
387
- );
388
- await smartshellInstance.exec(
389
- `git push origin ${currentBranch} --follow-tags`,
390
- );
391
- ui.printStep(
392
- currentStep,
393
- totalSteps,
394
- `🚀 Pushing to origin/${currentBranch}`,
395
- "done",
396
- );
232
+ async function runPushStep(
233
+ smartshellInstance: plugins.smartshell.Smartshell,
234
+ workflow: IResolvedCommitWorkflow,
235
+ ): Promise<void> {
236
+ const currentBranch = await detectCurrentBranch(smartshellInstance);
237
+ const followTags = workflow.pushFollowTags ? " --follow-tags" : "";
238
+ const result = await smartshellInstance.exec(
239
+ `git push ${workflow.pushRemote} ${currentBranch}${followTags}`,
240
+ );
241
+ if (result.exitCode !== 0) {
242
+ logger.log("error", "git push failed.");
243
+ process.exit(1);
397
244
  }
245
+ }
398
246
 
399
- // Step 7: Publish to npm registries (optional)
400
- let releasedRegistries: string[] = [];
401
- if (willRelease && releaseConfig) {
402
- currentStep++;
403
- const registries = releaseConfig.getRegistries();
404
- ui.printStep(
405
- currentStep,
406
- totalSteps,
407
- `📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
408
- "in-progress",
409
- );
410
-
411
- const accessLevel = releaseConfig.getAccessLevel();
412
- for (const registry of registries) {
413
- try {
414
- await smartshellInstance.exec(
415
- `npm publish --registry=${registry} --access=${accessLevel}`,
416
- );
417
- releasedRegistries.push(registry);
418
- } catch (error) {
419
- logger.log("error", `Failed to publish to ${registry}: ${error}`);
420
- }
421
- }
247
+ async function detectCurrentBranch(
248
+ smartshellInstance: plugins.smartshell.Smartshell,
249
+ ): Promise<string> {
250
+ const branchResult = await smartshellInstance.exec("git branch --show-current");
251
+ return branchResult.stdout.trim() || "master";
252
+ }
422
253
 
423
- if (releasedRegistries.length === registries.length) {
424
- ui.printStep(
425
- currentStep,
426
- totalSteps,
427
- `📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
428
- "done",
429
- );
430
- } else {
431
- ui.printStep(
432
- currentStep,
433
- totalSteps,
434
- `📦 Publishing to ${registries.length} registr${registries.length === 1 ? "y" : "ies"}`,
435
- "error",
436
- );
437
- }
254
+ function assertAnalysisComplete(
255
+ answerBucket: plugins.smartinteract.AnswerBucket | undefined,
256
+ nextCommitObject: any,
257
+ ): void {
258
+ if (!answerBucket || !nextCommitObject) {
259
+ throw new Error("Commit workflow requires analyze before changelog and commit steps.");
438
260
  }
261
+ }
439
262
 
440
- console.log(""); // Add spacing before summary
441
-
442
- // Get commit SHA for summary
443
- const commitShaResult = await smartshellInstance.exec(
444
- "git rev-parse --short HEAD",
445
- );
446
- const commitSha = commitShaResult.stdout.trim();
263
+ function shellQuote(value: string): string {
264
+ return `'${value.replaceAll("'", "'\\''")}'`;
265
+ }
447
266
 
448
- // Print final summary
449
- ui.printSummary({
450
- projectType,
451
- branch: currentBranch,
452
- commitType: answerBucket.getAnswerFor("commitType"),
453
- commitScope: answerBucket.getAnswerFor("commitScope"),
454
- commitMessage: answerBucket.getAnswerFor("commitDescription"),
455
- newVersion: newVersion,
456
- commitSha: commitSha,
457
- pushed: willPush,
458
- released: releasedRegistries.length > 0,
459
- releasedRegistries:
460
- releasedRegistries.length > 0 ? releasedRegistries : undefined,
461
- });
462
- };
267
+ function printCommitExecutionPlan(workflow: IResolvedCommitWorkflow): void {
268
+ console.log("");
269
+ console.log("gitzone commit - resolved workflow");
270
+ console.log(`confirmation: ${workflow.confirmation}`);
271
+ console.log(`steps: ${workflow.steps.join(" -> ")}`);
272
+ console.log(`changelog: ${workflow.changelogFile}#${workflow.changelogSection}`);
273
+ console.log("");
274
+ }
463
275
 
464
276
  async function handleRecommend(mode: ICliMode): Promise<void> {
465
277
  const recommendationBuilder = async () => {
@@ -507,40 +319,27 @@ export function showHelp(mode?: ICliMode): void {
507
319
  printJson({
508
320
  command: "commit",
509
321
  usage: "gitzone commit [recommend] [options]",
510
- description:
511
- "Creates semantic commits or emits a read-only recommendation.",
322
+ description: "Analyzes changes and creates one semantic source commit.",
512
323
  commands: [
513
324
  {
514
325
  name: "recommend",
515
- description:
516
- "Generate a commit recommendation without mutating the repository",
326
+ description: "Generate a commit recommendation without mutating the repository",
517
327
  },
518
328
  ],
519
329
  flags: [
520
- { flag: "-y, --yes", description: "Auto-accept AI recommendations" },
521
- { flag: "-p, --push", description: "Push to origin after commit" },
522
- { flag: "-t, --test", description: "Run tests before the commit flow" },
523
- {
524
- flag: "-b, --build",
525
- description: "Run the build after the commit flow",
526
- },
527
- {
528
- flag: "-r, --release",
529
- description: "Publish to configured registries after push",
530
- },
531
- {
532
- flag: "--format",
533
- description: "Run gitzone format before committing",
534
- },
535
- {
536
- flag: "--json",
537
- description: "Emit JSON for `commit recommend` only",
538
- },
330
+ { flag: "-y, --yes", description: "Auto-accept safe AI recommendations" },
331
+ { flag: "-p, --push", description: "Push to origin after committing" },
332
+ { flag: "-t, --test", description: "Run tests as part of the commit workflow" },
333
+ { flag: "-b, --build", description: "Run build as part of the commit workflow" },
334
+ { flag: "-f, --format", description: "Run gitzone format before committing" },
335
+ { flag: "--plan", description: "Show resolved workflow without mutating files" },
336
+ { flag: "--json", description: "Emit JSON for `commit recommend` only" },
539
337
  ],
540
338
  examples: [
541
339
  "gitzone commit recommend --json",
542
340
  "gitzone commit -y",
543
- "gitzone commit -ypbr",
341
+ "gitzone commit -ytbp",
342
+ "gitzone release",
544
343
  ],
545
344
  });
546
345
  return;
@@ -549,25 +348,24 @@ export function showHelp(mode?: ICliMode): void {
549
348
  console.log("");
550
349
  console.log("Usage: gitzone commit [recommend] [options]");
551
350
  console.log("");
351
+ console.log("Creates one semantic source commit. It does not version, tag, or publish.");
352
+ console.log("");
552
353
  console.log("Commands:");
553
- console.log(
554
- " recommend Generate a commit recommendation without mutating the repository",
555
- );
354
+ console.log(" recommend Generate a commit recommendation without mutating the repository");
556
355
  console.log("");
557
356
  console.log("Flags:");
558
- console.log(" -y, --yes Auto-accept AI recommendations");
559
- console.log(" -p, --push Push to origin after commit");
560
- console.log(" -t, --test Run tests before the commit flow");
561
- console.log(" -b, --build Run the build after the commit flow");
562
- console.log(
563
- " -r, --release Publish to configured registries after push",
564
- );
565
- console.log(" --format Run gitzone format before committing");
357
+ console.log(" -y, --yes Auto-accept safe AI recommendations");
358
+ console.log(" -p, --push Push after commit");
359
+ console.log(" -t, --test Run tests in the configured order");
360
+ console.log(" -b, --build Run build in the configured order");
361
+ console.log(" -f, --format Run gitzone format before committing");
362
+ console.log(" --plan Show resolved workflow without mutating files");
566
363
  console.log(" --json Emit JSON for `commit recommend` only");
567
364
  console.log("");
568
365
  console.log("Examples:");
569
366
  console.log(" gitzone commit recommend --json");
570
367
  console.log(" gitzone commit -y");
571
- console.log(" gitzone commit -ypbr");
368
+ console.log(" gitzone commit -ytbp");
369
+ console.log(" gitzone release");
572
370
  console.log("");
573
371
  }