maiass 5.13.0 → 5.14.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.
@@ -0,0 +1,54 @@
1
+ // Pure argv parsing helpers shared between maiass.mjs and unit tests.
2
+ //
3
+ // Kept dependency-free and side-effect-free so it can be imported in a unit
4
+ // test without triggering maiass.mjs's top-level CLI bootstrap.
5
+
6
+ /**
7
+ * Extract the -m / --message commit-message value from argv (MAI-51 parity).
8
+ *
9
+ * Supported forms (single value only — NOT git-style repeated -m):
10
+ * -m <value>
11
+ * --message <value>
12
+ * --message=<value>
13
+ *
14
+ * The value is taken VERBATIM (no escape-sequence interpretation, no newline
15
+ * collapsing). Only the whole-string leading/trailing whitespace is trimmed,
16
+ * and that trimming happens at the point of use (lib/commit.js) so that empty /
17
+ * whitespace-only values still fall through to the "No commit message provided"
18
+ * path. Here we return the raw string.
19
+ *
20
+ * Last occurrence wins if the flag appears more than once.
21
+ *
22
+ * @param {string[]} argv
23
+ * @returns {{ message: string|null, valueIndices: Set<number> }}
24
+ * message — the raw message string, or null if -m/--message not present.
25
+ * valueIndices — argv positions of the VALUE token only (the space-separated
26
+ * form's value). The flag tokens (-m/--message) are NOT included
27
+ * because (a) they start with '-' so command derivation already
28
+ * skips them, and (b) the flag-validator still needs to validate
29
+ * them against the allow-list. The value token is recorded so
30
+ * command derivation doesn't mistake it for a command/bump type
31
+ * and so the validator skips it if it happens to start with '-'.
32
+ */
33
+ export function extractMessageFlag(argv) {
34
+ let message = null;
35
+ const valueIndices = new Set();
36
+ for (let i = 0; i < argv.length; i++) {
37
+ const arg = argv[i];
38
+ if (arg === '-m' || arg === '--message') {
39
+ if (i + 1 < argv.length) {
40
+ message = argv[i + 1];
41
+ valueIndices.add(i + 1);
42
+ } else {
43
+ // Flag given with no following token — treat as empty so it falls
44
+ // through to the "No commit message provided" error rather than
45
+ // silently consuming nothing.
46
+ message = '';
47
+ }
48
+ } else if (arg.startsWith('--message=')) {
49
+ message = arg.slice('--message='.length);
50
+ valueIndices.add(i);
51
+ }
52
+ }
53
+ return { message, valueIndices };
54
+ }
package/lib/bootstrap.js CHANGED
@@ -612,6 +612,11 @@ MAIASS_STAGINGBRANCH=${config.branches.staging}
612
612
 
613
613
  # Example: Personal debug settings
614
614
  #MAIASS_DEBUG=true
615
+
616
+ # Example: devlog tagging (optional)
617
+ #MAIASS_DEVLOG_CLIENT="yourclient"
618
+ #MAIASS_DEVLOG_SUBCLIENT=""
619
+ #MAIASS_DEVLOG_PROJECT="yourproject"
615
620
  `;
616
621
  fs.writeFileSync('.env.maiass.local', localContent, 'utf8');
617
622
  log.success(SYMBOLS.CHECKMARK, 'Created .env.maiass.local for personal settings');
package/lib/commit.js CHANGED
@@ -628,20 +628,47 @@ async function getMultiLineCommitMessage(jiraTicket) {
628
628
  * @returns {Promise<string>} Final commit message
629
629
  */
630
630
  async function getCommitMessage(gitInfo, options = {}) {
631
- const { silent = false } = options;
632
-
631
+ const { silent = false, providedMessage = null } = options;
632
+
633
633
  // Load environment config to ensure tokens are loaded from secure storage
634
634
  loadEnvironmentConfig();
635
-
635
+
636
636
  const aiMode = process.env.MAIASS_AI_MODE || 'ask';
637
637
  const maiassToken = process.env.MAIASS_AI_TOKEN;
638
638
  const jiraTicket = gitInfo.jiraTicket;
639
-
639
+
640
640
  // Display JIRA ticket if found
641
641
  if (jiraTicket) {
642
642
  log.info(SYMBOLS.INFO, `Jira Ticket Number: ${jiraTicket}`);
643
643
  }
644
-
644
+
645
+ // -m / --message: verbatim, non-interactive commit message (MAI-XX parity).
646
+ //
647
+ // When supplied, this short-circuits BEFORE any AI proxy call and before any
648
+ // interactive prompt — so it works with no token and no credit, and never
649
+ // hits the network. The message is used EXACTLY as given except for trimming
650
+ // leading/trailing whitespace of the whole string: internal newlines,
651
+ // indentation and blank lines are preserved verbatim (do NOT reformat — the
652
+ // canonical "title\n - bullet" layout with no blank line between title and
653
+ // bullets must survive intact for changelog parsing). Escape sequences are
654
+ // NOT interpreted (a literal "\n" stays literal).
655
+ //
656
+ // Empty / whitespace-only → return '' so the caller emits the existing
657
+ // "No commit message provided" error and does not commit.
658
+ if (providedMessage !== null && providedMessage !== undefined) {
659
+ const trimmed = providedMessage.trim();
660
+ if (!trimmed) {
661
+ return '';
662
+ }
663
+ // Prepend the Jira ticket if detected and not already present (matches the
664
+ // AI / manual paths). The caller (handleStagedCommit) also guards against
665
+ // double-prepend via startsWith, so this is idempotent.
666
+ if (jiraTicket && !trimmed.startsWith(jiraTicket)) {
667
+ return `${jiraTicket} ${trimmed}`;
668
+ }
669
+ return trimmed;
670
+ }
671
+
645
672
  let useAI = false;
646
673
  let aiSuggestion = null;
647
674
 
@@ -787,27 +814,34 @@ async function getCommitMessage(gitInfo, options = {}) {
787
814
  * @returns {Promise<boolean>} True if commit was successful
788
815
  */
789
816
  async function handleStagedCommit(gitInfo, options = {}) {
790
- const { silent = false } = options;
791
- // Check if there are actually staged changes
792
- if (!gitInfo.status || gitInfo.status.stagedCount === 0) {
817
+ const { silent = false, dryRun = false, forceStaged = false, providedMessage = null } = options;
818
+ // Check if there are actually staged changes.
819
+ // In dry-run with forceStaged, nothing was really staged (the `git add` was
820
+ // only previewed), so skip this guard — the working-tree changes are what
821
+ // *would* be committed.
822
+ if (!forceStaged && (!gitInfo.status || gitInfo.status.stagedCount === 0)) {
793
823
  log.warning(SYMBOLS.INFO, 'Nothing to commit, working tree clean');
794
824
  return true;
795
825
  }
796
-
797
- // Show staged changes
798
- const stagedOutput = executeGitCommand('git diff --cached --name-status', true);
826
+
827
+ // Show the changes that are (or would be) committed. For a forced dry run
828
+ // nothing is actually staged, so preview the working-tree changes instead.
829
+ const diffCommand = forceStaged
830
+ ? 'git diff HEAD --name-status'
831
+ : 'git diff --cached --name-status';
832
+ const stagedOutput = executeGitCommand(diffCommand, true);
799
833
  if (!stagedOutput) {
800
834
  log.warning(SYMBOLS.INFO, 'No staged changes to show');
801
835
  return true;
802
836
  }
803
-
804
- log.critical(SYMBOLS.INFO, 'Staged changes detected:');
805
-
837
+
838
+ log.critical(SYMBOLS.INFO, forceStaged ? 'Changes that would be committed:' : 'Staged changes detected:');
839
+
806
840
  // Display the staged changes
807
841
  console.log(stagedOutput);
808
-
842
+
809
843
  // Get commit message
810
- const commitMessage = await getCommitMessage(gitInfo, { silent });
844
+ const commitMessage = await getCommitMessage(gitInfo, { silent, providedMessage });
811
845
  if (!commitMessage) {
812
846
  log.error(SYMBOLS.CROSS, 'No commit message provided');
813
847
  return false;
@@ -826,24 +860,28 @@ async function handleStagedCommit(gitInfo, options = {}) {
826
860
  if (jiraTicket && finalCommitMessage && !finalCommitMessage.startsWith(jiraTicket)) {
827
861
  finalCommitMessage = `${jiraTicket} ${finalCommitMessage}`;
828
862
  }
829
- // Write commit message to a temp file on all platforms — avoids shell quoting and injection risks
830
- {
863
+ if (dryRun) {
864
+ // Preview the commit instead of performing it
865
+ const previewMessage = finalCommitMessage.split('\n')[0];
866
+ console.log(colors.BBlue(`${SYMBOLS.INFO} Dry run - would commit with message: "${previewMessage}"`));
867
+ } else {
868
+ // Write commit message to a temp file on all platforms — avoids shell quoting and injection risks
831
869
  const tmpFile = path.join(os.tmpdir(), `maiass-commit-msg-${Date.now()}.txt`);
832
870
  fs.writeFileSync(tmpFile, finalCommitMessage, { encoding: 'utf8' });
833
871
  result = executeGitCommand(`git commit -F "${tmpFile}"`, quietMode);
834
872
  fs.unlinkSync(tmpFile);
873
+
874
+ if (result === null) {
875
+ log.error(SYMBOLS.CROSS, 'Commit failed');
876
+ return false;
877
+ }
878
+
879
+ log.success(SYMBOLS.CHECKMARK, 'Changes committed successfully');
880
+
881
+ // Log the commit to devlog.sh (equivalent to logthis in maiass.sh)
882
+ logCommit(commitMessage, gitInfo);
835
883
  }
836
-
837
- if (result === null) {
838
- log.error(SYMBOLS.CROSS, 'Commit failed');
839
- return false;
840
- }
841
-
842
- log.success(SYMBOLS.CHECKMARK, 'Changes committed successfully');
843
-
844
- // Log the commit to devlog.sh (equivalent to logthis in maiass.sh)
845
- logCommit(commitMessage, gitInfo);
846
-
884
+
847
885
  // Ask about pushing to remote
848
886
  if (remoteExists('origin')) {
849
887
  let reply;
@@ -872,13 +910,17 @@ async function handleStagedCommit(gitInfo, options = {}) {
872
910
  }
873
911
 
874
912
  if (reply === 'y') {
875
-
876
- const pushResult = executeGitCommand(`git push --set-upstream origin ${gitInfo.branch}`, false);
877
- if (pushResult !== null) {
878
- log.success(SYMBOLS.CHECKMARK, 'Commit pushed.');
913
+ if (dryRun) {
914
+ // Preview the push instead of performing it
915
+ console.log(colors.BBlue(`${SYMBOLS.INFO} Dry run - would push to origin ${gitInfo.branch} (git push --set-upstream origin ${gitInfo.branch})`));
879
916
  } else {
880
- log.error(SYMBOLS.CROSS, 'Push failed');
881
- return false;
917
+ const pushResult = executeGitCommand(`git push --set-upstream origin ${gitInfo.branch}`, false);
918
+ if (pushResult !== null) {
919
+ log.success(SYMBOLS.CHECKMARK, 'Commit pushed.');
920
+ } else {
921
+ log.error(SYMBOLS.CROSS, 'Push failed');
922
+ return false;
923
+ }
882
924
  }
883
925
  }
884
926
  } else {
@@ -898,7 +940,7 @@ async function handleStagedCommit(gitInfo, options = {}) {
898
940
  * @returns {Promise<boolean>} True if process completed successfully
899
941
  */
900
942
  export async function commitThis(options = {}) {
901
- const { autoStage = false, commitsOnly = false, silent = false } = options;
943
+ const { autoStage = false, commitsOnly = false, silent = false, dryRun = false, providedMessage = null } = options;
902
944
 
903
945
  // Get git information
904
946
  const gitInfo = getGitInfo();
@@ -947,21 +989,26 @@ export async function commitThis(options = {}) {
947
989
  reply = await getSingleCharInput('Do you want to stage and commit them? [y/N] ');
948
990
  }
949
991
  if (reply === 'y') {
992
+ if (dryRun) {
993
+ // Preview the stage instead of performing it
994
+ console.log(colors.BBlue(`${SYMBOLS.INFO} Dry run - would stage all changes (git add -A)`));
995
+ return await handleStagedCommit(gitInfo, { silent, dryRun, forceStaged: true, providedMessage });
996
+ }
950
997
  // Stage all changes
951
998
  const stageResult = executeGitCommand('git add -A', false);
952
999
  if (stageResult === null) {
953
1000
  log.error(SYMBOLS.CROSS, 'Failed to stage changes');
954
1001
  return false;
955
1002
  }
956
-
1003
+
957
1004
  // Refresh git info to get updated status
958
1005
  const updatedGitInfo = getGitInfo();
959
- return await handleStagedCommit(updatedGitInfo, { silent });
1006
+ return await handleStagedCommit(updatedGitInfo, { silent, dryRun, providedMessage });
960
1007
  } else {
961
1008
  // Check if there are staged changes to commit
962
1009
  if (status.stagedCount > 0) {
963
1010
  log.info(SYMBOLS.INFO, 'Proceeding with staged changes only');
964
- return await handleStagedCommit(gitInfo, { silent });
1011
+ return await handleStagedCommit(gitInfo, { silent, dryRun, providedMessage });
965
1012
  }
966
1013
 
967
1014
  // Handle the case where user declined to stage and there are no staged changes
@@ -979,20 +1026,25 @@ export async function commitThis(options = {}) {
979
1026
  }
980
1027
  }
981
1028
  } else {
1029
+ if (dryRun) {
1030
+ // Preview the stage instead of performing it
1031
+ console.log(colors.BBlue(`${SYMBOLS.INFO} Dry run - would stage all changes (git add -A)`));
1032
+ return await handleStagedCommit(gitInfo, { silent, dryRun, forceStaged: true, providedMessage });
1033
+ }
982
1034
  // Auto-stage all changes
983
1035
  const stageResult = executeGitCommand('git add -A', false);
984
1036
  if (stageResult === null) {
985
1037
  console.log(colors.Red(`${SYMBOLS.CROSS} Failed to stage changes`));
986
1038
  return false;
987
1039
  }
988
-
1040
+
989
1041
  // Refresh git info and commit
990
1042
  const updatedGitInfo = getGitInfo();
991
- return await handleStagedCommit(updatedGitInfo, { silent });
1043
+ return await handleStagedCommit(updatedGitInfo, { silent, dryRun, providedMessage });
992
1044
  }
993
1045
  } else if (status.stagedCount > 0) {
994
1046
  // Only staged changes present, proceed directly to commit
995
- return await handleStagedCommit(gitInfo, { silent });
1047
+ return await handleStagedCommit(gitInfo, { silent, dryRun, providedMessage });
996
1048
  }
997
1049
 
998
1050
  return true;
@@ -89,13 +89,19 @@ export function suggestFlag(unknown, candidates) {
89
89
  * Validate the argv flags for a given subcommand.
90
90
  * @param {string[]} args - The raw argv tail (process.argv.slice(2))
91
91
  * @param {string[]} subcommandFlags - FLAGS export from the active subcommand handler
92
+ * @param {Set<number>} [ignoreIndices] - argv positions to skip entirely. Used
93
+ * for option VALUES that may legitimately start with '-' (e.g. the -m/--message
94
+ * value, where a caller might pass `-m "-- not a flag --"`). Without this, such
95
+ * a value token would be misread as an unknown flag and rejected.
92
96
  * @returns {{valid: true} | {valid: false, flag: string, suggestion: string|null, validFlags: string[]}}
93
97
  */
94
- export function validateFlags(args, subcommandFlags = []) {
98
+ export function validateFlags(args, subcommandFlags = [], ignoreIndices = new Set()) {
95
99
  // Dedupe so callers don't have to worry about overlap between the always-
96
100
  // valid set and a subcommand's FLAGS (e.g. account-info also lists --json).
97
101
  const validFlags = Array.from(new Set([...ALWAYS_VALID_FLAGS, ...subcommandFlags]));
98
- for (const arg of args) {
102
+ for (let i = 0; i < args.length; i++) {
103
+ const arg = args[i];
104
+ if (ignoreIndices.has(i)) continue;
99
105
  if (!arg.startsWith('-')) continue;
100
106
  // Support `--tag=value` form — only the name portion is validated.
101
107
  const flagName = arg.split('=')[0];
@@ -19,6 +19,10 @@ export const FLAGS = [
19
19
  '--force', '-f',
20
20
  '--silent', '-s',
21
21
  '--tag', '-t',
22
+ // -m / --message <value>: supply the commit message verbatim, non-interactively
23
+ // (MAI-XX node+bash parity). Bypasses AI + interactive prompt. Works with the
24
+ // default workflow and commits-only (-c / -ac).
25
+ '--message', '-m',
22
26
  ];
23
27
 
24
28
  /**
@@ -34,7 +38,9 @@ export async function handleMaiassCommand(args) {
34
38
  'dry-run': dryRun,
35
39
  tag,
36
40
  force,
37
- silent
41
+ silent,
42
+ // Verbatim commit message from -m/--message (MAI-XX). null when not supplied.
43
+ message: providedMessage = null
38
44
  } = args;
39
45
 
40
46
  // Extract version bump from positional arguments if provided
@@ -63,7 +69,8 @@ export async function handleMaiassCommand(args) {
63
69
  dryRun,
64
70
  tag,
65
71
  force,
66
- silent
72
+ silent,
73
+ providedMessage
67
74
  });
68
75
 
69
76
  if (result.success) {
@@ -246,16 +246,18 @@ async function validateAndHandleBranching(options = {}) {
246
246
  * @returns {Promise<Object>} Commit result
247
247
  */
248
248
  async function handleCommitWorkflow(branchInfo, options = {}) {
249
- const { commitsOnly = false, autoStage = false, silent = false } = options;
250
-
249
+ const { commitsOnly = false, autoStage = false, silent = false, dryRun = false, providedMessage = null } = options;
250
+
251
251
  logger.header(SYMBOLS.INFO, 'Commit Workflow Phase');
252
-
252
+
253
253
  try {
254
254
  // Run the commit workflow
255
255
  const commitSuccess = await commitThis({
256
256
  autoStage,
257
257
  commitsOnly,
258
- silent
258
+ silent,
259
+ dryRun,
260
+ providedMessage
259
261
  });
260
262
 
261
263
  // Check if commit workflow succeeded
@@ -829,7 +831,9 @@ export async function runMaiassPipeline(options = {}) {
829
831
  dryRun = false,
830
832
  tag = false,
831
833
  force = false,
832
- silent = false
834
+ silent = false,
835
+ // Verbatim commit message from -m/--message (MAI-XX). null = not supplied.
836
+ providedMessage = null
833
837
  } = options;
834
838
 
835
839
  // Display branded header with MAIASS's own version (not project version)
@@ -867,7 +871,7 @@ export async function runMaiassPipeline(options = {}) {
867
871
 
868
872
  // Phase 2: Commit Workflow
869
873
  logger.debug('Phase 2: Commit Workflow');
870
- const commitResult = await handleCommitWorkflow(branchInfo, { commitsOnly, autoStage, silent });
874
+ const commitResult = await handleCommitWorkflow(branchInfo, { commitsOnly, autoStage, silent, dryRun, providedMessage });
871
875
 
872
876
  if (!commitResult.success) {
873
877
  // Check if it was cancelled by user vs actual failure
@@ -886,9 +890,13 @@ export async function runMaiassPipeline(options = {}) {
886
890
  // If commits-only mode, stop here
887
891
  if (commitsOnly) {
888
892
  console.log();
889
- console.log(colors.BGreen(`${SYMBOLS.CHECKMARK} Commits-only mode completed successfully`));
893
+ if (dryRun) {
894
+ console.log(colors.BGreen(`${SYMBOLS.CHECKMARK} Commits-only mode dry run completed (no changes made)`));
895
+ } else {
896
+ console.log(colors.BGreen(`${SYMBOLS.CHECKMARK} Commits-only mode completed successfully`));
897
+ }
890
898
  console.log(colors.BBlue(`${SYMBOLS.INFO} Current branch: ${getCurrentBranch()}`));
891
- return { success: true, phase: 'commits-only' };
899
+ return { success: true, phase: 'commits-only', dryRun };
892
900
  }
893
901
 
894
902
  console.log();
@@ -33,7 +33,7 @@ export const MAIASS_VARIABLES = {
33
33
  'MAIASS_VERSION_PRIMARY_FILE': { default: '', description: 'Primary version file path' },
34
34
  'MAIASS_VERSION_PRIMARY_TYPE': { default: '', description: 'Primary version file type' },
35
35
  'MAIASS_VERSION_PRIMARY_LINE_START': { default: '', description: 'Line start pattern for version' },
36
- 'MAIASS_VERSION_SECONDARY_FILES': { default: '', description: 'Secondary version files (comma-separated)' },
36
+ 'MAIASS_VERSION_SECONDARY_FILES': { default: '', description: 'Secondary version files (pipe-separated file:type:pattern entries)' },
37
37
 
38
38
  // Branch configuration
39
39
  'MAIASS_DEVELOPBRANCH': { default: 'develop', description: 'Development branch name' },
package/maiass.mjs CHANGED
@@ -55,11 +55,25 @@ import { SYMBOLS } from './lib/symbols.js';
55
55
  import { bootstrapProject } from './lib/bootstrap.js';
56
56
  import { createGithubAction, showGitlabExcerpt, showBitbucketExcerpt } from './lib/ci-templates.js';
57
57
  import { validateFlags } from './lib/flag-validator.js';
58
+ import { extractMessageFlag } from './lib/arg-utils.js';
58
59
 
59
60
  // Simple CLI setup for pkg compatibility
60
61
  const args = process.argv.slice(2);
61
- // Skip flags (starting with -) to find the first meaningful argument
62
- const firstArg = args.find(a => !a.startsWith('-'));
62
+
63
+ // -m / --message <value>: a verbatim commit message supplied non-interactively
64
+ // (MAI-XX node+bash parity). The flag CONSUMES the next token as its value, so
65
+ // that value must NOT be treated as the command name / version-bump type during
66
+ // command derivation below. Supported forms: `-m <value>`, `--message <value>`,
67
+ // `--message=<value>`. Single value only (no git-style repeated -m).
68
+ //
69
+ // extractMessageFlag returns { message, valueIndices } where valueIndices marks
70
+ // argv positions that belong to the flag (the flag token and, for the space-
71
+ // separated form, its value) so command derivation can skip them.
72
+ const { message: providedMessage, valueIndices: messageArgIndices } = extractMessageFlag(args);
73
+
74
+ // Skip flags (starting with -) AND the -m/--message value token to find the
75
+ // first meaningful positional argument (command / version-bump).
76
+ const firstArg = args.find((a, i) => !a.startsWith('-') && !messageArgIndices.has(i));
63
77
 
64
78
  // Handle --setup/--bootstrap flag early
65
79
  if (args.includes('--setup') || args.includes('--bootstrap')) {
@@ -146,7 +160,10 @@ const SUBCOMMAND_FLAGS = {
146
160
  };
147
161
 
148
162
  const activeSubcommandFlags = SUBCOMMAND_FLAGS[command] ?? [];
149
- const flagValidation = validateFlags(args, activeSubcommandFlags);
163
+ // Skip the -m/--message value token during validation — it may legitimately
164
+ // begin with '-' (e.g. `-m "-- breaking change --"`) and must not be treated
165
+ // as an unknown flag.
166
+ const flagValidation = validateFlags(args, activeSubcommandFlags, messageArgIndices);
150
167
 
151
168
  if (!flagValidation.valid) {
152
169
  console.error(colors.Red(`${SYMBOLS.CROSS} Error: Unrecognized option '${flagValidation.flag}' for command '${command}'`));
@@ -167,6 +184,14 @@ if (!flagValidation.valid) {
167
184
  process.exit(1);
168
185
  }
169
186
 
187
+ // An explicitly-supplied but empty/whitespace-only -m/--message is a usage
188
+ // error — fail fast with a non-zero exit (parity with bash, which aborts here).
189
+ // An absent flag (providedMessage === null) is unaffected.
190
+ if (providedMessage !== null && providedMessage.trim() === '') {
191
+ console.error(colors.Red(`${SYMBOLS.CROSS} Error: --message/-m requires a non-empty value`));
192
+ process.exit(1);
193
+ }
194
+
170
195
  // Apply auto-mode env vars now that validation has passed. Setting these
171
196
  // before validation would leak MAIASS_AUTO_* into process.env for commands
172
197
  // that reject --auto (e.g. `account-info --auto`) — see MAI-43 code review.
@@ -221,6 +246,8 @@ if (args.includes('--help') || args.includes('-h') || command === 'help') {
221
246
  console.log(' --auto, -a Full auto — stage, commit, push, merge to develop, bump version');
222
247
  console.log(' --auto-commit, -ac Auto-yes for commit phase only — stops after commit (no merge, no bump)');
223
248
  console.log(' --commits-only, -c Generate AI commits without version management');
249
+ console.log(' --message, -m <msg> Use this commit message verbatim (skips AI + the message prompt; no credit/token needed).');
250
+ console.log(' Supplies the message only; for fully unattended use combine with -ac (or --auto-stage).');
224
251
  console.log(' --auto-stage Automatically stage all changes');
225
252
  console.log(' --setup, --bootstrap Run interactive project setup');
226
253
  console.log(' --help, -h Show this help message');
@@ -263,7 +290,12 @@ if (args.includes('--show-bb-excerpt')) { showBitbucketExcerpt(); process.exit(
263
290
  '.env.maiass.local',
264
291
  `# .env.maiass.local — personal/local MAIASS settings (never committed)\n` +
265
292
  `# Generated on first run: ${new Date().toISOString()}\n` +
266
- `# Use this file for personal overrides, e.g. MAIASS_AI_MODE=autosuggest\n`,
293
+ `# Use this file for personal overrides, e.g. MAIASS_AI_MODE=autosuggest\n` +
294
+ `\n` +
295
+ `# Example: devlog tagging (optional)\n` +
296
+ `#MAIASS_DEVLOG_CLIENT="yourclient"\n` +
297
+ `#MAIASS_DEVLOG_SUBCLIENT=""\n` +
298
+ `#MAIASS_DEVLOG_PROJECT="yourproject"\n`,
267
299
  'utf8'
268
300
  );
269
301
 
@@ -326,7 +358,9 @@ if (args.includes('--show-bb-excerpt')) { showBitbucketExcerpt(); process.exit(
326
358
  case 'maiass':
327
359
  // Handle the main MAIASS workflow
328
360
  await handleMaiassCommand({
329
- _: process.argv.slice(2).filter(arg => !arg.startsWith('-')),
361
+ // Positionals must exclude the -m/--message value token (it doesn't start
362
+ // with '-' but is NOT a command/bump-type positional).
363
+ _: process.argv.slice(2).filter((arg, i) => !arg.startsWith('-') && !messageArgIndices.has(i)),
330
364
  // -ac is equivalent to -c (commits-only) plus auto-yes prompts
331
365
  'commits-only': args.includes('--commits-only') || args.includes('-c') || args.includes('--auto-commit') || args.includes('-ac'),
332
366
  'auto-stage': args.includes('--auto-stage'),
@@ -335,7 +369,10 @@ if (args.includes('--show-bb-excerpt')) { showBitbucketExcerpt(); process.exit(
335
369
  'dry-run': args.includes('--dry-run') || args.includes('-d'),
336
370
  force: args.includes('--force') || args.includes('-f'),
337
371
  silent: args.includes('--silent') || args.includes('-s'),
338
- tag: getArgValue(args, '--tag') || getArgValue(args, '-t')
372
+ tag: getArgValue(args, '--tag') || getArgValue(args, '-t'),
373
+ // Verbatim commit message supplied via -m/--message (or null). Threaded
374
+ // through the pipeline to the commit phase; bypasses AI + interactive.
375
+ message: providedMessage
339
376
  });
340
377
  break;
341
378
 
@@ -363,3 +400,6 @@ function getArgValue(args, flag) {
363
400
  }
364
401
  return null;
365
402
  }
403
+
404
+ // extractMessageFlag is imported from ./lib/arg-utils.js — extracted (MAI-51)
405
+ // so it can be unit-tested without booting the CLI on import.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "maiass",
3
3
  "type": "module",
4
- "version": "5.13.0",
4
+ "version": "5.14.0",
5
5
  "description": "AI commit messages, version bumps, and changelogs from one command. Stages, commits, merges, tags. Anonymous on first run — no email, no card.",
6
6
  "main": "maiass.mjs",
7
7
  "bin": {
@@ -50,8 +50,11 @@ jobs:
50
50
 
51
51
  - uses: actions/setup-node@v4
52
52
  with:
53
- node-version: '20'
54
- cache: 'npm'
53
+ node-version: '24'
54
+ # No cache: this workflow only does `npm install -g maiass`, which
55
+ # needs no project-level npm cache. Setting cache: 'npm' makes
56
+ # setup-node fail on projects without a Node lock file (e.g.
57
+ # WordPress/PHP plugins): "Dependencies lock file is not found".
55
58
 
56
59
  - name: Install maiass
57
60
  run: npm install -g maiass --no-fund --no-audit