@git.zone/cli 6.11.0 → 6.12.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.
@@ -5,15 +5,27 @@ import * as paths from "../paths.js";
5
5
  import { logger } from "../gitzone.logging.js";
6
6
  import * as ui from "./mod.ui.js";
7
7
  import type { ICliMode } 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";
8
+ import {
9
+ getCliMode,
10
+ printJson,
11
+ runWithSuppressedOutput,
12
+ } from "../helpers.climode.js";
13
+ import {
14
+ appendPendingChangelogEntry,
15
+ appendPendingChangelogMarkdown,
16
+ } from "../helpers.changelog.js";
17
+ import {
18
+ resolveCommitWorkflow,
19
+ type IResolvedCommitWorkflow,
20
+ } from "../helpers.workflow.js";
21
+ import {
22
+ hasManualCommitInput,
23
+ loadManualCommitInput,
24
+ type IManualCommitInput,
25
+ } from "./helpers.manual.js";
11
26
 
12
27
  const isNoChangesError = (error: unknown): boolean => {
13
- return (
14
- error instanceof plugins.tsdoc.NoChangesError ||
15
- (error instanceof Error && error.name === "NoChangesError")
16
- );
28
+ return error instanceof Error && error.name === "NoChangesError";
17
29
  };
18
30
 
19
31
  export const run = async (argvArg: any) => {
@@ -26,6 +38,8 @@ export const run = async (argvArg: any) => {
26
38
  }
27
39
 
28
40
  if (subcommand === "recommend") {
41
+ if (hasManualCommitInput(argvArg))
42
+ throw new Error("Manual input cannot be combined with commit recommend.");
29
43
  await handleRecommend(mode);
30
44
  return;
31
45
  }
@@ -39,7 +53,11 @@ export const run = async (argvArg: any) => {
39
53
  return;
40
54
  }
41
55
 
42
- const workflow = await resolveCommitWorkflow(argvArg);
56
+ const manual = await loadManualCommitInput(argvArg, paths.cwd);
57
+ const workflow = await resolveCommitWorkflow(
58
+ argvArg,
59
+ manual ? "manual" : "ai",
60
+ );
43
61
  if (workflow.releaseFlagRequested) {
44
62
  logger.log(
45
63
  "warn",
@@ -47,7 +65,7 @@ export const run = async (argvArg: any) => {
47
65
  );
48
66
  }
49
67
 
50
- printCommitExecutionPlan(workflow);
68
+ printCommitExecutionPlan(workflow, manual);
51
69
  if (workflow.confirmation === "plan") {
52
70
  return;
53
71
  }
@@ -59,6 +77,8 @@ export const run = async (argvArg: any) => {
59
77
 
60
78
  let nextCommitObject: any;
61
79
  let answerBucket: plugins.smartinteract.AnswerBucket | undefined;
80
+ if (manual && !(await hasManualCommitChanges(smartshellInstance))) return;
81
+ if (manual && !(await confirmManualCommit(manual, workflow, mode))) return;
62
82
 
63
83
  for (const step of workflow.steps) {
64
84
  switch (step) {
@@ -66,30 +86,69 @@ export const run = async (argvArg: any) => {
66
86
  await runFormatStep();
67
87
  break;
68
88
  case "test":
69
- await runCommandStep(smartshellInstance, "Running tests", workflow.testCommand);
89
+ await runCommandStep(
90
+ smartshellInstance,
91
+ "Running tests",
92
+ workflow.testCommand,
93
+ );
70
94
  break;
71
95
  case "build":
72
- await runCommandStep(smartshellInstance, "Running build", workflow.buildCommand);
96
+ await runCommandStep(
97
+ smartshellInstance,
98
+ "Running build",
99
+ workflow.buildCommand,
100
+ );
73
101
  break;
74
102
  case "analyze":
75
103
  try {
76
104
  nextCommitObject = await runAnalyzeStep();
77
105
  } catch (error) {
78
106
  if (isNoChangesError(error)) {
79
- logger.log("info", "No uncommitted changes found. Nothing to commit.");
107
+ logger.log(
108
+ "info",
109
+ "No uncommitted changes found. Nothing to commit.",
110
+ );
80
111
  return;
81
112
  }
82
113
  throw error;
83
114
  }
84
- answerBucket = await buildAnswerBucket(nextCommitObject, workflow, mode, argvArg);
115
+ answerBucket = await buildAnswerBucket(
116
+ nextCommitObject,
117
+ workflow,
118
+ mode,
119
+ argvArg,
120
+ );
121
+ break;
122
+ case "manual": {
123
+ if (!manual) throw new Error("Manual commit input is required.");
124
+ if (!(await hasManualCommitChanges(smartshellInstance))) return;
125
+ nextCommitObject = { manual: true };
126
+ answerBucket = createAnswerBucket({
127
+ commitType: manual.type,
128
+ commitScope: manual.scope,
129
+ commitDescription: manual.summary,
130
+ });
85
131
  break;
132
+ }
86
133
  case "changelog":
87
134
  assertAnalysisComplete(answerBucket, nextCommitObject);
88
- await runChangelogStep(workflow, answerBucket!, nextCommitObject);
135
+ if (manual) {
136
+ await appendPendingChangelogMarkdown(
137
+ plugins.path.join(paths.cwd, workflow.changelogFile),
138
+ workflow.changelogSection,
139
+ manual.changelog,
140
+ );
141
+ logger.log(
142
+ "success",
143
+ `Updated ${workflow.changelogFile} pending section.`,
144
+ );
145
+ } else {
146
+ await runChangelogStep(workflow, answerBucket!, nextCommitObject);
147
+ }
89
148
  break;
90
149
  case "commit":
91
150
  assertAnalysisComplete(answerBucket, nextCommitObject);
92
- await runCommitStep(smartshellInstance, answerBucket!);
151
+ await runCommitStep(smartshellInstance, answerBucket!, manual?.message);
93
152
  break;
94
153
  case "push":
95
154
  await runPushStep(smartshellInstance, workflow);
@@ -97,7 +156,9 @@ export const run = async (argvArg: any) => {
97
156
  }
98
157
  }
99
158
 
100
- const commitShaResult = await smartshellInstance.exec("git rev-parse --short HEAD");
159
+ const commitShaResult = await smartshellInstance.exec(
160
+ "git rev-parse --short HEAD",
161
+ );
101
162
  const currentBranch = await detectCurrentBranch(smartshellInstance);
102
163
  ui.printSummary({
103
164
  projectType: "source",
@@ -110,6 +171,46 @@ export const run = async (argvArg: any) => {
110
171
  });
111
172
  };
112
173
 
174
+ async function hasManualCommitChanges(
175
+ smartshellInstance: plugins.smartshell.Smartshell,
176
+ ): Promise<boolean> {
177
+ const status = await smartshellInstance.execSpawn(
178
+ "git",
179
+ ["status", "--porcelain=v1", "--untracked-files=all"],
180
+ { cwd: paths.cwd, silent: true },
181
+ );
182
+ if (status.exitCode !== 0)
183
+ throw new Error("Cannot inspect the repository for a manual commit.");
184
+ if (status.stdout.trim()) return true;
185
+ logger.log("info", "No uncommitted changes found. Nothing to commit.");
186
+ return false;
187
+ }
188
+
189
+ async function confirmManualCommit(
190
+ manual: IManualCommitInput,
191
+ workflow: IResolvedCommitWorkflow,
192
+ mode: ICliMode,
193
+ ): Promise<boolean> {
194
+ if (
195
+ workflow.confirmation === "auto" &&
196
+ (!manual.breaking || workflow.allowBreaking)
197
+ )
198
+ return true;
199
+ if (!mode.interactive) {
200
+ throw new Error(
201
+ manual.breaking
202
+ ? "BREAKING CHANGE confirmation requires an interactive terminal. Use `-y --allow-breaking` to auto-accept it."
203
+ : "Commit confirmation requires an interactive terminal. Use `-y` or set commit.confirmation to `auto`.",
204
+ );
205
+ }
206
+ return plugins.smartinteract.SmartInteract.getCliConfirmation(
207
+ manual.breaking
208
+ ? "Create this breaking commit and changelog entry?"
209
+ : "Create this commit and changelog entry?",
210
+ false,
211
+ );
212
+ }
213
+
113
214
  async function runFormatStep(): Promise<void> {
114
215
  ui.printHeader("Formatting project files");
115
216
  const formatMod = await import("../mod_format/index.js");
@@ -132,7 +233,8 @@ async function runCommandStep(
132
233
 
133
234
  async function runAnalyzeStep(): Promise<any> {
134
235
  ui.printHeader("Analyzing repository changes");
135
- const aidoc = new plugins.tsdoc.AiDoc();
236
+ const tsdoc = await plugins.loadTsdoc();
237
+ const aidoc = new tsdoc.AiDoc();
136
238
  await aidoc.start();
137
239
  try {
138
240
  const nextCommitObject = await aidoc.buildNextCommitObject(paths.cwd);
@@ -140,7 +242,8 @@ async function runAnalyzeStep(): Promise<any> {
140
242
  recommendedNextVersion: nextCommitObject.recommendedNextVersion,
141
243
  recommendedNextVersionLevel: nextCommitObject.recommendedNextVersionLevel,
142
244
  recommendedNextVersionScope: nextCommitObject.recommendedNextVersionScope,
143
- recommendedNextVersionMessage: nextCommitObject.recommendedNextVersionMessage,
245
+ recommendedNextVersionMessage:
246
+ nextCommitObject.recommendedNextVersionMessage,
144
247
  });
145
248
  return nextCommitObject;
146
249
  } finally {
@@ -154,8 +257,11 @@ async function buildAnswerBucket(
154
257
  mode: ICliMode,
155
258
  argvArg: any,
156
259
  ): Promise<plugins.smartinteract.AnswerBucket> {
157
- const isBreakingChange = nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
158
- const canAutoAccept = workflow.confirmation === "auto" && (!isBreakingChange || workflow.allowBreaking);
260
+ const isBreakingChange =
261
+ nextCommitObject.recommendedNextVersionLevel === "BREAKING CHANGE";
262
+ const canAutoAccept =
263
+ workflow.confirmation === "auto" &&
264
+ (!isBreakingChange || workflow.allowBreaking);
159
265
 
160
266
  if (canAutoAccept) {
161
267
  logger.log(
@@ -171,15 +277,25 @@ async function buildAnswerBucket(
171
277
  });
172
278
  }
173
279
 
174
- if (isBreakingChange && (workflow.confirmation === "auto" || argvArg.y || argvArg.yes)) {
175
- logger.log("warn", "BREAKING CHANGE detected - manual confirmation required");
280
+ if (
281
+ isBreakingChange &&
282
+ (workflow.confirmation === "auto" || argvArg.y || argvArg.yes)
283
+ ) {
284
+ logger.log(
285
+ "warn",
286
+ "BREAKING CHANGE detected - manual confirmation required",
287
+ );
176
288
  }
177
289
 
178
290
  if (!mode.interactive) {
179
291
  if (isBreakingChange) {
180
- throw new Error("BREAKING CHANGE confirmation requires an interactive terminal. Use `-y --allow-breaking` to auto-accept it.");
292
+ throw new Error(
293
+ "BREAKING CHANGE confirmation requires an interactive terminal. Use `-y --allow-breaking` to auto-accept it.",
294
+ );
181
295
  }
182
- throw new Error("Commit confirmation requires an interactive terminal. Use `-y` or set commit.confirmation to `auto`.");
296
+ throw new Error(
297
+ "Commit confirmation requires an interactive terminal. Use `-y` or set commit.confirmation to `auto`.",
298
+ );
183
299
  }
184
300
 
185
301
  const commitInteract = new plugins.smartinteract.SmartInteract();
@@ -240,15 +356,39 @@ async function runChangelogStep(
240
356
  async function runCommitStep(
241
357
  smartshellInstance: plugins.smartshell.Smartshell,
242
358
  answerBucket: plugins.smartinteract.AnswerBucket,
359
+ manualMessage?: string,
243
360
  ): Promise<void> {
244
361
  ui.printHeader("Creating Semantic Commit");
245
- const commitString = createCommitStringFromAnswerBucket(answerBucket);
362
+ const commitString =
363
+ manualMessage ?? createCommitStringFromAnswerBucket(answerBucket);
246
364
  ui.printCommitMessage(commitString);
247
- await smartshellInstance.exec("git add -A");
248
- const result = await smartshellInstance.exec(`git commit -m ${shellQuote(commitString)}`);
249
- if (result.exitCode !== 0) {
250
- logger.log("error", "git commit failed.");
251
- process.exit(1);
365
+ let messageDirectory: string | undefined;
366
+ try {
367
+ let commitArgs = ["commit", "-m", commitString];
368
+ if (manualMessage !== undefined) {
369
+ messageDirectory = await plugins.fs.mkdtemp(
370
+ plugins.path.join(plugins.os.tmpdir(), "gitzone-commit-message-"),
371
+ );
372
+ const messagePath = plugins.path.join(messageDirectory, "message.txt");
373
+ await plugins.fs.writeFile(messagePath, `${commitString}\n`, {
374
+ encoding: "utf8",
375
+ mode: 0o600,
376
+ flag: "wx",
377
+ });
378
+ commitArgs = ["commit", "--cleanup=verbatim", "--file", messagePath];
379
+ }
380
+ const staged = await smartshellInstance.execSpawn("git", ["add", "-A"], {
381
+ cwd: paths.cwd,
382
+ });
383
+ if (staged.exitCode !== 0)
384
+ throw new Error("git add failed; no commit was attempted.");
385
+ const result = await smartshellInstance.execSpawn("git", commitArgs, {
386
+ cwd: paths.cwd,
387
+ });
388
+ if (result.exitCode !== 0) throw new Error("git commit failed.");
389
+ } finally {
390
+ if (messageDirectory)
391
+ await plugins.fs.rm(messageDirectory, { recursive: true, force: true });
252
392
  }
253
393
  }
254
394
 
@@ -270,7 +410,9 @@ async function runPushStep(
270
410
  async function detectCurrentBranch(
271
411
  smartshellInstance: plugins.smartshell.Smartshell,
272
412
  ): Promise<string> {
273
- const branchResult = await smartshellInstance.exec("git branch --show-current");
413
+ const branchResult = await smartshellInstance.exec(
414
+ "git branch --show-current",
415
+ );
274
416
  return branchResult.stdout.trim() || "master";
275
417
  }
276
418
 
@@ -279,26 +421,35 @@ function assertAnalysisComplete(
279
421
  nextCommitObject: any,
280
422
  ): void {
281
423
  if (!answerBucket || !nextCommitObject) {
282
- throw new Error("Commit workflow requires analyze before changelog and commit steps.");
424
+ throw new Error(
425
+ "Commit workflow requires analyze before changelog and commit steps.",
426
+ );
283
427
  }
284
428
  }
285
429
 
286
- function shellQuote(value: string): string {
287
- return `'${value.replaceAll("'", "'\\''")}'`;
288
- }
289
-
290
- function printCommitExecutionPlan(workflow: IResolvedCommitWorkflow): void {
430
+ function printCommitExecutionPlan(
431
+ workflow: IResolvedCommitWorkflow,
432
+ manual?: IManualCommitInput,
433
+ ): void {
291
434
  console.log("");
292
435
  console.log("gitzone commit - resolved workflow");
293
436
  console.log(`confirmation: ${workflow.confirmation}`);
437
+ console.log(`input: ${manual ? "manual" : "ai"}`);
294
438
  console.log(`steps: ${workflow.steps.join(" -> ")}`);
295
- console.log(`changelog: ${workflow.changelogFile}#${workflow.changelogSection}`);
439
+ console.log(
440
+ `changelog: ${workflow.changelogFile}#${workflow.changelogSection}`,
441
+ );
442
+ if (manual) {
443
+ ui.printCommitMessage(manual.message);
444
+ console.log(manual.changelog);
445
+ }
296
446
  console.log("");
297
447
  }
298
448
 
299
449
  async function handleRecommend(mode: ICliMode): Promise<void> {
300
450
  const recommendationBuilder = async () => {
301
- const aidoc = new plugins.tsdoc.AiDoc();
451
+ const tsdoc = await plugins.loadTsdoc();
452
+ const aidoc = new tsdoc.AiDoc();
302
453
  await aidoc.start();
303
454
  try {
304
455
  return await aidoc.buildNextCommitObject(paths.cwd);
@@ -355,24 +506,66 @@ export function showHelp(mode?: ICliMode): void {
355
506
  printJson({
356
507
  command: "commit",
357
508
  usage: "gitzone commit [recommend] [options]",
358
- description: "Analyzes changes and creates one semantic source commit.",
509
+ description:
510
+ "Creates one semantic source commit from AI recommendations or explicit manual input.",
359
511
  commands: [
360
512
  {
361
513
  name: "recommend",
362
- description: "Generate a commit recommendation without mutating the repository",
514
+ description:
515
+ "Generate a commit recommendation without mutating the repository",
363
516
  },
364
517
  ],
365
518
  flags: [
366
- { flag: "-y, --yes", description: "Auto-accept safe AI recommendations" },
367
- { flag: "--allow-breaking", description: "Allow -y/--yes to accept BREAKING CHANGE recommendations" },
519
+ {
520
+ flag: "-m, --message <text>",
521
+ description: "Use a complete semantic commit message without AI",
522
+ },
523
+ {
524
+ flag: "--message-file <path>",
525
+ description: "Read the complete semantic message from a UTF-8 file",
526
+ },
527
+ {
528
+ flag: "--changelog <text>",
529
+ description:
530
+ "Use custom Pending entry text or Markdown with a manual message",
531
+ },
532
+ {
533
+ flag: "--changelog-file <path>",
534
+ description: "Read custom Pending text or Markdown from a UTF-8 file",
535
+ },
536
+ {
537
+ flag: "-y, --yes",
538
+ description: "Confirm safe AI or manual commit input",
539
+ },
540
+ {
541
+ flag: "--allow-breaking",
542
+ description: "Allow -y/--yes to accept breaking AI or manual input",
543
+ },
368
544
  { flag: "-p, --push", description: "Push to origin after committing" },
369
- { flag: "-t, --test", description: "Run tests as part of the commit workflow" },
370
- { flag: "-b, --build", description: "Run build as part of the commit workflow" },
371
- { flag: "-f, --format", description: "Run gitzone format before committing" },
372
- { flag: "--plan", description: "Show resolved workflow without mutating files" },
373
- { flag: "--json", description: "Emit JSON for `commit recommend` only" },
545
+ {
546
+ flag: "-t, --test",
547
+ description: "Run tests as part of the commit workflow",
548
+ },
549
+ {
550
+ flag: "-b, --build",
551
+ description: "Run build as part of the commit workflow",
552
+ },
553
+ {
554
+ flag: "-f, --format",
555
+ description: "Run gitzone format before committing",
556
+ },
557
+ {
558
+ flag: "--plan",
559
+ description: "Show resolved workflow without mutating files",
560
+ },
561
+ {
562
+ flag: "--json",
563
+ description: "Emit JSON for `commit recommend` only",
564
+ },
374
565
  ],
375
566
  examples: [
567
+ "gitzone commit -y --message 'fix(cache): expire stale entries'",
568
+ "gitzone commit -y --message-file /tmp/commit.txt --changelog-file /tmp/pending.md",
376
569
  "gitzone commit recommend --json",
377
570
  "gitzone commit -y",
378
571
  "gitzone commit -ytbp",
@@ -385,22 +578,48 @@ export function showHelp(mode?: ICliMode): void {
385
578
  console.log("");
386
579
  console.log("Usage: gitzone commit [recommend] [options]");
387
580
  console.log("");
388
- console.log("Creates one semantic source commit. It does not version, tag, or publish.");
581
+ console.log(
582
+ "Creates one semantic source commit. It does not version, tag, or publish.",
583
+ );
389
584
  console.log("");
390
585
  console.log("Commands:");
391
- console.log(" recommend Generate a commit recommendation without mutating the repository");
586
+ console.log(
587
+ " recommend Generate a commit recommendation without mutating the repository",
588
+ );
392
589
  console.log("");
393
590
  console.log("Flags:");
394
- console.log(" -y, --yes Auto-accept safe AI recommendations");
395
- console.log(" --allow-breaking Allow -y/--yes to accept BREAKING CHANGE recommendations");
591
+ console.log(
592
+ " -m, --message TEXT Use a complete semantic message without AI",
593
+ );
594
+ console.log(
595
+ " --message-file PATH Read the complete message from a UTF-8 file",
596
+ );
597
+ console.log(
598
+ " --changelog TEXT Custom Pending entry text or Markdown (requires a manual message)",
599
+ );
600
+ console.log(
601
+ " --changelog-file PATH Read custom Pending text or Markdown from a UTF-8 file",
602
+ );
603
+ console.log(" -y, --yes Confirm safe AI or manual commit input");
604
+ console.log(
605
+ " --allow-breaking Allow -y/--yes to accept breaking AI or manual input",
606
+ );
396
607
  console.log(" -p, --push Push after commit");
397
608
  console.log(" -t, --test Run tests in the configured order");
398
609
  console.log(" -b, --build Run build in the configured order");
399
610
  console.log(" -f, --format Run gitzone format before committing");
400
- console.log(" --plan Show resolved workflow without mutating files");
611
+ console.log(
612
+ " --plan Show resolved workflow without mutating files",
613
+ );
401
614
  console.log(" --json Emit JSON for `commit recommend` only");
402
615
  console.log("");
403
616
  console.log("Examples:");
617
+ console.log(
618
+ " gitzone commit -y --message 'fix(cache): expire stale entries'",
619
+ );
620
+ console.log(
621
+ " gitzone commit -y --message-file /tmp/commit.txt --changelog-file /tmp/pending.md",
622
+ );
404
623
  console.log(" gitzone commit recommend --json");
405
624
  console.log(" gitzone commit -y");
406
625
  console.log(" gitzone commit -ytbp");
@@ -1,10 +1,11 @@
1
- export * from '../plugins.js';
1
+ export * from "../plugins.js";
2
2
 
3
- import * as commitinfo from '@push.rocks/commitinfo';
4
- import * as smartfile from '@push.rocks/smartfile';
5
- import * as smartinteract from '@push.rocks/smartinteract';
6
- import * as smartshell from '@push.rocks/smartshell';
3
+ import * as commitinfo from "@push.rocks/commitinfo";
4
+ import * as smartfile from "@push.rocks/smartfile";
5
+ import * as smartinteract from "@push.rocks/smartinteract";
6
+ import * as smartshell from "@push.rocks/smartshell";
7
7
 
8
- import * as tsdoc from '@git.zone/tsdoc';
8
+ // Manual commits must not load or initialize the AI dependency chain.
9
+ export const loadTsdoc = () => import("@git.zone/tsdoc");
9
10
 
10
- export { commitinfo, smartfile, smartinteract, smartshell, tsdoc };
11
+ export { commitinfo, smartfile, smartinteract, smartshell };
@@ -17,7 +17,7 @@ const commandSummaries: ICommandHelpSummary[] = [
17
17
  {
18
18
  name: "commit",
19
19
  description:
20
- "Analyze changes and create semantic source commits",
20
+ "Create semantic source commits from AI or manual input",
21
21
  },
22
22
  { name: "release", description: "Create versioned releases from pending changelog entries" },
23
23
  { name: "format", description: "Plan or apply project formatting changes" },