maiass 5.13.0 → 5.14.2
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/lib/arg-utils.js +54 -0
- package/lib/bootstrap.js +5 -0
- package/lib/commit.js +157 -43
- package/lib/config-command.js +1 -1
- package/lib/config-manager.js +1 -1
- package/lib/flag-validator.js +8 -2
- package/lib/maiass-command.js +9 -2
- package/lib/maiass-pipeline.js +16 -8
- package/lib/maiass-variables.js +2 -1
- package/maiass.mjs +46 -6
- package/package.json +1 -1
- package/templates/ci/github-version-bump.yml +5 -2
package/lib/arg-utils.js
ADDED
|
@@ -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
|
@@ -16,6 +16,45 @@ import { logCommit } from './devlog.js';
|
|
|
16
16
|
import colors from './colors.js';
|
|
17
17
|
import chalk from 'chalk';
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Pick the AI model used for commit message generation.
|
|
21
|
+
*
|
|
22
|
+
* Resolution order:
|
|
23
|
+
* 1. MAIASS_AI_COMMIT_MODEL — explicit per-commit override. Wins absolutely.
|
|
24
|
+
* 2. MAIASS_AI_MODEL — legacy global override. Preserves prior behaviour for
|
|
25
|
+
* users who configured it explicitly.
|
|
26
|
+
* 3. Auto-pick by diff size (post-truncation char count of the staged diff
|
|
27
|
+
* that will actually be sent to the model):
|
|
28
|
+
* < 30000 chars -> gpt-4o-mini (tier 1)
|
|
29
|
+
* 30000..100000 -> gpt-4-turbo (tier 2)
|
|
30
|
+
* > 100000 -> gpt-4o (tier 3)
|
|
31
|
+
*
|
|
32
|
+
* In practice MAIASS_AI_MAX_CHARACTERS defaults to 8000, so tier 1 is the
|
|
33
|
+
* common case. Tiers 2 and 3 only fire when a user has raised that cap. The
|
|
34
|
+
* ladder is forward-compatible with future increases.
|
|
35
|
+
*
|
|
36
|
+
* Returns { model, tier, source } where source is 'override' | 'legacy' |
|
|
37
|
+
* 'auto', and tier is 1/2/3 (only meaningful when source === 'auto').
|
|
38
|
+
*/
|
|
39
|
+
export function pickCommitModel(diffLength, env = process.env) {
|
|
40
|
+
const override = env.MAIASS_AI_COMMIT_MODEL;
|
|
41
|
+
if (override && override.trim()) {
|
|
42
|
+
return { model: override.trim(), tier: null, source: 'override' };
|
|
43
|
+
}
|
|
44
|
+
const legacy = env.MAIASS_AI_MODEL;
|
|
45
|
+
if (legacy && legacy.trim()) {
|
|
46
|
+
return { model: legacy.trim(), tier: null, source: 'legacy' };
|
|
47
|
+
}
|
|
48
|
+
const len = typeof diffLength === 'number' && diffLength >= 0 ? diffLength : 0;
|
|
49
|
+
if (len < 30000) {
|
|
50
|
+
return { model: 'gpt-4o-mini', tier: 1, source: 'auto' };
|
|
51
|
+
}
|
|
52
|
+
if (len <= 100000) {
|
|
53
|
+
return { model: 'gpt-4-turbo', tier: 2, source: 'auto' };
|
|
54
|
+
}
|
|
55
|
+
return { model: 'gpt-4o', tier: 3, source: 'auto' };
|
|
56
|
+
}
|
|
57
|
+
|
|
19
58
|
/**
|
|
20
59
|
* Simple spinner for AI API calls
|
|
21
60
|
*/
|
|
@@ -291,7 +330,8 @@ async function getAICommitSuggestion(gitInfo) {
|
|
|
291
330
|
const aiHost = process.env.MAIASS_AI_HOST || 'https://pound.maiass.net';
|
|
292
331
|
const aiPath = process.env.MAIASS_AI_PATH || '/proxy';
|
|
293
332
|
const aiEndpoint = aiHost + aiPath;
|
|
294
|
-
|
|
333
|
+
// aiModel is resolved AFTER the diff is collected and truncated — the
|
|
334
|
+
// auto-pick layer needs the final diff size. See pickCommitModel() below.
|
|
295
335
|
const aiTemperature = parseFloat(process.env.MAIASS_AI_TEMPERATURE || '0.7');
|
|
296
336
|
const commitMessageStyle = process.env.MAIASS_AI_COMMIT_MESSAGE_STYLE || 'bullet';
|
|
297
337
|
const maxCharacters = parseInt(process.env.MAIASS_AI_MAX_CHARACTERS || '8000');
|
|
@@ -372,7 +412,29 @@ async function getAICommitSuggestion(gitInfo) {
|
|
|
372
412
|
log.info(SYMBOLS.INFO, `Git diff truncated to ${maxCharacters} characters`);
|
|
373
413
|
}
|
|
374
414
|
|
|
415
|
+
// Pick the AI model AFTER truncation — the auto-pick layer reads the final
|
|
416
|
+
// diff size that will actually be sent to the model. MAIASS_AI_COMMIT_MODEL
|
|
417
|
+
// overrides everything; MAIASS_AI_MODEL is the legacy fallback; otherwise
|
|
418
|
+
// we ladder based on diff length.
|
|
419
|
+
const modelPick = pickCommitModel(gitDiff.length);
|
|
420
|
+
const aiModel = modelPick.model;
|
|
421
|
+
|
|
375
422
|
if (debugMode) {
|
|
423
|
+
let pickSummary;
|
|
424
|
+
if (modelPick.source === 'override') {
|
|
425
|
+
pickSummary = `[MAIASS DEBUG] AI commit model: ${aiModel} (override via MAIASS_AI_COMMIT_MODEL)`;
|
|
426
|
+
} else if (modelPick.source === 'legacy') {
|
|
427
|
+
pickSummary = `[MAIASS DEBUG] AI commit model: ${aiModel} (legacy MAIASS_AI_MODEL)`;
|
|
428
|
+
} else {
|
|
429
|
+
const ranges = {
|
|
430
|
+
1: '< 30000',
|
|
431
|
+
2: '30000-100000',
|
|
432
|
+
3: '> 100000'
|
|
433
|
+
};
|
|
434
|
+
pickSummary = `[MAIASS DEBUG] AI commit model: ${aiModel} (tier ${modelPick.tier}, diff ${gitDiff.length} chars, threshold ${ranges[modelPick.tier]})`;
|
|
435
|
+
}
|
|
436
|
+
log.debug(SYMBOLS.INFO, pickSummary);
|
|
437
|
+
|
|
376
438
|
log.debug(SYMBOLS.INFO, '[MAIASS DEBUG] --- AI Commit Suggestion Context ---');
|
|
377
439
|
log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] Model: ${aiModel}`);
|
|
378
440
|
log.debug(SYMBOLS.INFO, `[MAIASS DEBUG] Temperature: ${aiTemperature}`);
|
|
@@ -628,20 +690,47 @@ async function getMultiLineCommitMessage(jiraTicket) {
|
|
|
628
690
|
* @returns {Promise<string>} Final commit message
|
|
629
691
|
*/
|
|
630
692
|
async function getCommitMessage(gitInfo, options = {}) {
|
|
631
|
-
const { silent = false } = options;
|
|
632
|
-
|
|
693
|
+
const { silent = false, providedMessage = null } = options;
|
|
694
|
+
|
|
633
695
|
// Load environment config to ensure tokens are loaded from secure storage
|
|
634
696
|
loadEnvironmentConfig();
|
|
635
|
-
|
|
697
|
+
|
|
636
698
|
const aiMode = process.env.MAIASS_AI_MODE || 'ask';
|
|
637
699
|
const maiassToken = process.env.MAIASS_AI_TOKEN;
|
|
638
700
|
const jiraTicket = gitInfo.jiraTicket;
|
|
639
|
-
|
|
701
|
+
|
|
640
702
|
// Display JIRA ticket if found
|
|
641
703
|
if (jiraTicket) {
|
|
642
704
|
log.info(SYMBOLS.INFO, `Jira Ticket Number: ${jiraTicket}`);
|
|
643
705
|
}
|
|
644
|
-
|
|
706
|
+
|
|
707
|
+
// -m / --message: verbatim, non-interactive commit message (MAI-XX parity).
|
|
708
|
+
//
|
|
709
|
+
// When supplied, this short-circuits BEFORE any AI proxy call and before any
|
|
710
|
+
// interactive prompt — so it works with no token and no credit, and never
|
|
711
|
+
// hits the network. The message is used EXACTLY as given except for trimming
|
|
712
|
+
// leading/trailing whitespace of the whole string: internal newlines,
|
|
713
|
+
// indentation and blank lines are preserved verbatim (do NOT reformat — the
|
|
714
|
+
// canonical "title\n - bullet" layout with no blank line between title and
|
|
715
|
+
// bullets must survive intact for changelog parsing). Escape sequences are
|
|
716
|
+
// NOT interpreted (a literal "\n" stays literal).
|
|
717
|
+
//
|
|
718
|
+
// Empty / whitespace-only → return '' so the caller emits the existing
|
|
719
|
+
// "No commit message provided" error and does not commit.
|
|
720
|
+
if (providedMessage !== null && providedMessage !== undefined) {
|
|
721
|
+
const trimmed = providedMessage.trim();
|
|
722
|
+
if (!trimmed) {
|
|
723
|
+
return '';
|
|
724
|
+
}
|
|
725
|
+
// Prepend the Jira ticket if detected and not already present (matches the
|
|
726
|
+
// AI / manual paths). The caller (handleStagedCommit) also guards against
|
|
727
|
+
// double-prepend via startsWith, so this is idempotent.
|
|
728
|
+
if (jiraTicket && !trimmed.startsWith(jiraTicket)) {
|
|
729
|
+
return `${jiraTicket} ${trimmed}`;
|
|
730
|
+
}
|
|
731
|
+
return trimmed;
|
|
732
|
+
}
|
|
733
|
+
|
|
645
734
|
let useAI = false;
|
|
646
735
|
let aiSuggestion = null;
|
|
647
736
|
|
|
@@ -787,27 +876,34 @@ async function getCommitMessage(gitInfo, options = {}) {
|
|
|
787
876
|
* @returns {Promise<boolean>} True if commit was successful
|
|
788
877
|
*/
|
|
789
878
|
async function handleStagedCommit(gitInfo, options = {}) {
|
|
790
|
-
const { silent = false } = options;
|
|
791
|
-
// Check if there are actually staged changes
|
|
792
|
-
|
|
879
|
+
const { silent = false, dryRun = false, forceStaged = false, providedMessage = null } = options;
|
|
880
|
+
// Check if there are actually staged changes.
|
|
881
|
+
// In dry-run with forceStaged, nothing was really staged (the `git add` was
|
|
882
|
+
// only previewed), so skip this guard — the working-tree changes are what
|
|
883
|
+
// *would* be committed.
|
|
884
|
+
if (!forceStaged && (!gitInfo.status || gitInfo.status.stagedCount === 0)) {
|
|
793
885
|
log.warning(SYMBOLS.INFO, 'Nothing to commit, working tree clean');
|
|
794
886
|
return true;
|
|
795
887
|
}
|
|
796
|
-
|
|
797
|
-
// Show
|
|
798
|
-
|
|
888
|
+
|
|
889
|
+
// Show the changes that are (or would be) committed. For a forced dry run
|
|
890
|
+
// nothing is actually staged, so preview the working-tree changes instead.
|
|
891
|
+
const diffCommand = forceStaged
|
|
892
|
+
? 'git diff HEAD --name-status'
|
|
893
|
+
: 'git diff --cached --name-status';
|
|
894
|
+
const stagedOutput = executeGitCommand(diffCommand, true);
|
|
799
895
|
if (!stagedOutput) {
|
|
800
896
|
log.warning(SYMBOLS.INFO, 'No staged changes to show');
|
|
801
897
|
return true;
|
|
802
898
|
}
|
|
803
|
-
|
|
804
|
-
log.critical(SYMBOLS.INFO, 'Staged changes detected:');
|
|
805
|
-
|
|
899
|
+
|
|
900
|
+
log.critical(SYMBOLS.INFO, forceStaged ? 'Changes that would be committed:' : 'Staged changes detected:');
|
|
901
|
+
|
|
806
902
|
// Display the staged changes
|
|
807
903
|
console.log(stagedOutput);
|
|
808
|
-
|
|
904
|
+
|
|
809
905
|
// Get commit message
|
|
810
|
-
const commitMessage = await getCommitMessage(gitInfo, { silent });
|
|
906
|
+
const commitMessage = await getCommitMessage(gitInfo, { silent, providedMessage });
|
|
811
907
|
if (!commitMessage) {
|
|
812
908
|
log.error(SYMBOLS.CROSS, 'No commit message provided');
|
|
813
909
|
return false;
|
|
@@ -826,24 +922,28 @@ async function handleStagedCommit(gitInfo, options = {}) {
|
|
|
826
922
|
if (jiraTicket && finalCommitMessage && !finalCommitMessage.startsWith(jiraTicket)) {
|
|
827
923
|
finalCommitMessage = `${jiraTicket} ${finalCommitMessage}`;
|
|
828
924
|
}
|
|
829
|
-
|
|
830
|
-
|
|
925
|
+
if (dryRun) {
|
|
926
|
+
// Preview the commit instead of performing it
|
|
927
|
+
const previewMessage = finalCommitMessage.split('\n')[0];
|
|
928
|
+
console.log(colors.BBlue(`${SYMBOLS.INFO} Dry run - would commit with message: "${previewMessage}"`));
|
|
929
|
+
} else {
|
|
930
|
+
// Write commit message to a temp file on all platforms — avoids shell quoting and injection risks
|
|
831
931
|
const tmpFile = path.join(os.tmpdir(), `maiass-commit-msg-${Date.now()}.txt`);
|
|
832
932
|
fs.writeFileSync(tmpFile, finalCommitMessage, { encoding: 'utf8' });
|
|
833
933
|
result = executeGitCommand(`git commit -F "${tmpFile}"`, quietMode);
|
|
834
934
|
fs.unlinkSync(tmpFile);
|
|
935
|
+
|
|
936
|
+
if (result === null) {
|
|
937
|
+
log.error(SYMBOLS.CROSS, 'Commit failed');
|
|
938
|
+
return false;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
log.success(SYMBOLS.CHECKMARK, 'Changes committed successfully');
|
|
942
|
+
|
|
943
|
+
// Log the commit to devlog.sh (equivalent to logthis in maiass.sh)
|
|
944
|
+
logCommit(commitMessage, gitInfo);
|
|
835
945
|
}
|
|
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
|
-
|
|
946
|
+
|
|
847
947
|
// Ask about pushing to remote
|
|
848
948
|
if (remoteExists('origin')) {
|
|
849
949
|
let reply;
|
|
@@ -872,13 +972,17 @@ async function handleStagedCommit(gitInfo, options = {}) {
|
|
|
872
972
|
}
|
|
873
973
|
|
|
874
974
|
if (reply === 'y') {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
log.success(SYMBOLS.CHECKMARK, 'Commit pushed.');
|
|
975
|
+
if (dryRun) {
|
|
976
|
+
// Preview the push instead of performing it
|
|
977
|
+
console.log(colors.BBlue(`${SYMBOLS.INFO} Dry run - would push to origin ${gitInfo.branch} (git push --set-upstream origin ${gitInfo.branch})`));
|
|
879
978
|
} else {
|
|
880
|
-
|
|
881
|
-
|
|
979
|
+
const pushResult = executeGitCommand(`git push --set-upstream origin ${gitInfo.branch}`, false);
|
|
980
|
+
if (pushResult !== null) {
|
|
981
|
+
log.success(SYMBOLS.CHECKMARK, 'Commit pushed.');
|
|
982
|
+
} else {
|
|
983
|
+
log.error(SYMBOLS.CROSS, 'Push failed');
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
882
986
|
}
|
|
883
987
|
}
|
|
884
988
|
} else {
|
|
@@ -898,7 +1002,7 @@ async function handleStagedCommit(gitInfo, options = {}) {
|
|
|
898
1002
|
* @returns {Promise<boolean>} True if process completed successfully
|
|
899
1003
|
*/
|
|
900
1004
|
export async function commitThis(options = {}) {
|
|
901
|
-
const { autoStage = false, commitsOnly = false, silent = false } = options;
|
|
1005
|
+
const { autoStage = false, commitsOnly = false, silent = false, dryRun = false, providedMessage = null } = options;
|
|
902
1006
|
|
|
903
1007
|
// Get git information
|
|
904
1008
|
const gitInfo = getGitInfo();
|
|
@@ -947,21 +1051,26 @@ export async function commitThis(options = {}) {
|
|
|
947
1051
|
reply = await getSingleCharInput('Do you want to stage and commit them? [y/N] ');
|
|
948
1052
|
}
|
|
949
1053
|
if (reply === 'y') {
|
|
1054
|
+
if (dryRun) {
|
|
1055
|
+
// Preview the stage instead of performing it
|
|
1056
|
+
console.log(colors.BBlue(`${SYMBOLS.INFO} Dry run - would stage all changes (git add -A)`));
|
|
1057
|
+
return await handleStagedCommit(gitInfo, { silent, dryRun, forceStaged: true, providedMessage });
|
|
1058
|
+
}
|
|
950
1059
|
// Stage all changes
|
|
951
1060
|
const stageResult = executeGitCommand('git add -A', false);
|
|
952
1061
|
if (stageResult === null) {
|
|
953
1062
|
log.error(SYMBOLS.CROSS, 'Failed to stage changes');
|
|
954
1063
|
return false;
|
|
955
1064
|
}
|
|
956
|
-
|
|
1065
|
+
|
|
957
1066
|
// Refresh git info to get updated status
|
|
958
1067
|
const updatedGitInfo = getGitInfo();
|
|
959
|
-
return await handleStagedCommit(updatedGitInfo, { silent });
|
|
1068
|
+
return await handleStagedCommit(updatedGitInfo, { silent, dryRun, providedMessage });
|
|
960
1069
|
} else {
|
|
961
1070
|
// Check if there are staged changes to commit
|
|
962
1071
|
if (status.stagedCount > 0) {
|
|
963
1072
|
log.info(SYMBOLS.INFO, 'Proceeding with staged changes only');
|
|
964
|
-
return await handleStagedCommit(gitInfo, { silent });
|
|
1073
|
+
return await handleStagedCommit(gitInfo, { silent, dryRun, providedMessage });
|
|
965
1074
|
}
|
|
966
1075
|
|
|
967
1076
|
// Handle the case where user declined to stage and there are no staged changes
|
|
@@ -979,20 +1088,25 @@ export async function commitThis(options = {}) {
|
|
|
979
1088
|
}
|
|
980
1089
|
}
|
|
981
1090
|
} else {
|
|
1091
|
+
if (dryRun) {
|
|
1092
|
+
// Preview the stage instead of performing it
|
|
1093
|
+
console.log(colors.BBlue(`${SYMBOLS.INFO} Dry run - would stage all changes (git add -A)`));
|
|
1094
|
+
return await handleStagedCommit(gitInfo, { silent, dryRun, forceStaged: true, providedMessage });
|
|
1095
|
+
}
|
|
982
1096
|
// Auto-stage all changes
|
|
983
1097
|
const stageResult = executeGitCommand('git add -A', false);
|
|
984
1098
|
if (stageResult === null) {
|
|
985
1099
|
console.log(colors.Red(`${SYMBOLS.CROSS} Failed to stage changes`));
|
|
986
1100
|
return false;
|
|
987
1101
|
}
|
|
988
|
-
|
|
1102
|
+
|
|
989
1103
|
// Refresh git info and commit
|
|
990
1104
|
const updatedGitInfo = getGitInfo();
|
|
991
|
-
return await handleStagedCommit(updatedGitInfo, { silent });
|
|
1105
|
+
return await handleStagedCommit(updatedGitInfo, { silent, dryRun, providedMessage });
|
|
992
1106
|
}
|
|
993
1107
|
} else if (status.stagedCount > 0) {
|
|
994
1108
|
// Only staged changes present, proceed directly to commit
|
|
995
|
-
return await handleStagedCommit(gitInfo, { silent });
|
|
1109
|
+
return await handleStagedCommit(gitInfo, { silent, dryRun, providedMessage });
|
|
996
1110
|
}
|
|
997
1111
|
|
|
998
1112
|
return true;
|
package/lib/config-command.js
CHANGED
|
@@ -81,7 +81,7 @@ function displayConfig(config, options = {}) {
|
|
|
81
81
|
// Group by category for better display
|
|
82
82
|
const categories = {
|
|
83
83
|
'Core System': ['MAIASS_DEBUG', 'MAIASS_VERBOSITY', 'MAIASS_LOGGING', 'MAIASS_BRAND'],
|
|
84
|
-
'AI Integration': ['MAIASS_AI_MODE', 'MAIASS_AI_TOKEN', 'MAIASS_AI_MODEL', 'MAIASS_AI_TEMPERATURE', 'MAIASS_AI_HOST', 'MAIASS_AI_MAX_CHARACTERS', 'MAIASS_AI_COMMIT_MESSAGE_STYLE'],
|
|
84
|
+
'AI Integration': ['MAIASS_AI_MODE', 'MAIASS_AI_TOKEN', 'MAIASS_AI_MODEL', 'MAIASS_AI_COMMIT_MODEL', 'MAIASS_AI_TEMPERATURE', 'MAIASS_AI_HOST', 'MAIASS_AI_MAX_CHARACTERS', 'MAIASS_AI_COMMIT_MESSAGE_STYLE'],
|
|
85
85
|
'Git Branches': ['MAIASS_DEVELOPBRANCH', 'MAIASS_STAGINGBRANCH', 'MAIASS_MAINBRANCH'],
|
|
86
86
|
'Repository Settings': ['MAIASS_REPO_TYPE', 'MAIASS_GITHUB_OWNER', 'MAIASS_GITHUB_REPO', 'MAIASS_BITBUCKET_WORKSPACE', 'MAIASS_BITBUCKET_REPO_SLUG'],
|
|
87
87
|
'Pull Requests': ['MAIASS_DEVELOP_PULLREQUESTS'],
|
package/lib/config-manager.js
CHANGED
|
@@ -112,7 +112,7 @@ export function writeConfig(configPath, config, options = {}) {
|
|
|
112
112
|
// Group variables by category for better organization
|
|
113
113
|
const categories = {
|
|
114
114
|
'Core': ['MAIASS_DEBUG', 'MAIASS_VERBOSITY', 'MAIASS_LOGGING', 'MAIASS_BRAND'],
|
|
115
|
-
'AI': ['MAIASS_AI_MODE', 'MAIASS_AI_TOKEN', 'MAIASS_AI_MODEL', 'MAIASS_AI_TEMPERATURE', 'MAIASS_AI_ENDPOINT', 'MAIASS_AI_MAX_CHARACTERS', 'MAIASS_AI_COMMIT_MESSAGE_STYLE'],
|
|
115
|
+
'AI': ['MAIASS_AI_MODE', 'MAIASS_AI_TOKEN', 'MAIASS_AI_MODEL', 'MAIASS_AI_COMMIT_MODEL', 'MAIASS_AI_TEMPERATURE', 'MAIASS_AI_ENDPOINT', 'MAIASS_AI_MAX_CHARACTERS', 'MAIASS_AI_COMMIT_MESSAGE_STYLE'],
|
|
116
116
|
'Branches': ['MAIASS_DEVELOPBRANCH', 'MAIASS_STAGINGBRANCH', 'MAIASS_MAINBRANCH'],
|
|
117
117
|
'Repository': ['MAIASS_REPO_TYPE', 'MAIASS_GITHUB_OWNER', 'MAIASS_GITHUB_REPO', 'MAIASS_BITBUCKET_WORKSPACE', 'MAIASS_BITBUCKET_REPO_SLUG'],
|
|
118
118
|
'Pull Requests': ['MAIASS_DEVELOP_PULLREQUESTS'],
|
package/lib/flag-validator.js
CHANGED
|
@@ -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 (
|
|
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];
|
package/lib/maiass-command.js
CHANGED
|
@@ -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) {
|
package/lib/maiass-pipeline.js
CHANGED
|
@@ -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
|
-
|
|
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();
|
package/lib/maiass-variables.js
CHANGED
|
@@ -22,6 +22,7 @@ export const MAIASS_VARIABLES = {
|
|
|
22
22
|
'MAIASS_AI_MODE': { default: 'ask', description: 'AI interaction mode' },
|
|
23
23
|
'MAIASS_AI_TOKEN': { default: '', description: 'AI API token', sensitive: true },
|
|
24
24
|
'MAIASS_AI_MODEL': { default: 'gpt-3.5-turbo', description: 'AI model to use' },
|
|
25
|
+
'MAIASS_AI_COMMIT_MODEL': { default: '', description: 'Override the AI model used for commit message generation. If set, skips auto-pick. Defaults to MAIASS_AI_MODEL if set, otherwise auto-picks by diff size.' },
|
|
25
26
|
'MAIASS_AI_TEMPERATURE': { default: '0.7', description: 'AI temperature setting' },
|
|
26
27
|
'MAIASS_AI_MAX_CHARACTERS': { default: '8000', description: 'Max characters for AI requests' },
|
|
27
28
|
'MAIASS_AI_TIMEOUT': { default: '30', description: 'AI request timeout in seconds' },
|
|
@@ -33,7 +34,7 @@ export const MAIASS_VARIABLES = {
|
|
|
33
34
|
'MAIASS_VERSION_PRIMARY_FILE': { default: '', description: 'Primary version file path' },
|
|
34
35
|
'MAIASS_VERSION_PRIMARY_TYPE': { default: '', description: 'Primary version file type' },
|
|
35
36
|
'MAIASS_VERSION_PRIMARY_LINE_START': { default: '', description: 'Line start pattern for version' },
|
|
36
|
-
'MAIASS_VERSION_SECONDARY_FILES': { default: '', description: 'Secondary version files (
|
|
37
|
+
'MAIASS_VERSION_SECONDARY_FILES': { default: '', description: 'Secondary version files (pipe-separated file:type:pattern entries)' },
|
|
37
38
|
|
|
38
39
|
// Branch configuration
|
|
39
40
|
'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
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "5.14.2",
|
|
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: '
|
|
54
|
-
cache:
|
|
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
|